diff --git a/briar-android/src/org/briarproject/android/AndroidExecutorImpl.java b/briar-android/src/org/briarproject/android/AndroidExecutorImpl.java index 1fb060d68ee870e19bc4a8d492f6cbcf602a76de..e46e7193bad9dfc7fc0ab75d7d3af109ed109897 100644 --- a/briar-android/src/org/briarproject/android/AndroidExecutorImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidExecutorImpl.java @@ -1,87 +1,48 @@ package org.briarproject.android; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import org.briarproject.api.android.AndroidExecutor; - +import android.app.Application; +import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.Message; -class AndroidExecutorImpl implements AndroidExecutor { +import org.briarproject.api.android.AndroidExecutor; - private static final int SHUTDOWN = 0, RUN = 1; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; - private static final Logger LOG = - Logger.getLogger(AndroidExecutorImpl.class.getName()); +import javax.inject.Inject; - private final Runnable loop; - private final AtomicBoolean started = new AtomicBoolean(false); - private final CountDownLatch startLatch = new CountDownLatch(1); +class AndroidExecutorImpl implements AndroidExecutor { - private volatile Handler handler = null; + private final Handler handler; @Inject - AndroidExecutorImpl() { - loop = new Runnable() { - public void run() { - Looper.prepare(); - handler = new FutureTaskHandler(); - startLatch.countDown(); - Looper.loop(); - } - }; - } - - private void startIfNecessary() { - if (started.getAndSet(true)) return; - new Thread(loop, "AndroidExecutor").start(); - try { - startLatch.await(); - } catch (InterruptedException e) { - LOG.warning("Interrupted while starting executor thread"); - Thread.currentThread().interrupt(); - } + AndroidExecutorImpl(Application app) { + Context ctx = app.getApplicationContext(); + handler = new FutureTaskHandler(ctx.getMainLooper()); } - public <V> V call(Callable<V> c) throws InterruptedException, - ExecutionException { - startIfNecessary(); + public <V> Future<V> submit(Callable<V> c) { Future<V> f = new FutureTask<V>(c); - Message m = Message.obtain(handler, RUN, f); - handler.sendMessage(m); - return f.get(); + handler.sendMessage(Message.obtain(handler, 0, f)); + return f; } - public void shutdown() { - if (handler != null) { - Message m = Message.obtain(handler, SHUTDOWN); - handler.sendMessage(m); - } + public void execute(Runnable r) { + handler.post(r); } private static class FutureTaskHandler extends Handler { + private FutureTaskHandler(Looper looper) { + super(looper); + } + @Override public void handleMessage(Message m) { - switch(m.what) { - case SHUTDOWN: - Looper.myLooper().quit(); - break; - case RUN: - ((FutureTask<?>) m.obj).run(); - break; - default: - throw new IllegalArgumentException(); - } + ((FutureTask<?>) m.obj).run(); } } } diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java index 44e3649bd2c924c9dabc2b9ac19b10a28c3cd443..8a5bcba15e5acf951ffe05faed623fa53803d873 100644 --- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java @@ -14,23 +14,25 @@ import org.briarproject.android.contact.ConversationActivity; import org.briarproject.android.forum.ForumActivity; import org.briarproject.android.forum.ForumListActivity; import org.briarproject.api.Settings; +import org.briarproject.api.android.AndroidExecutor; import org.briarproject.api.android.AndroidNotificationManager; -import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DbException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.SettingsUpdatedEvent; +import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.messaging.MessagingManager; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; import org.briarproject.util.StringUtils; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; import javax.inject.Inject; @@ -44,7 +46,7 @@ import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static java.util.logging.Level.WARNING; class AndroidNotificationManagerImpl implements AndroidNotificationManager, -EventListener { + EventListener { private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3; private static final int FORUM_POST_NOTIFICATION_ID = 4; @@ -59,27 +61,33 @@ EventListener { private final DatabaseComponent db; private final Executor dbExecutor; private final EventBus eventBus; + private final MessagingManager messagingManager; + private final ForumManager forumManager; + private final AndroidExecutor androidExecutor; private final Context appContext; - private final Lock lock = new ReentrantLock(); - // The following are locking: lock - private final Map<ContactId, Integer> contactCounts = - new HashMap<ContactId, Integer>(); + // The following must only be accessed on the main UI thread + private final Map<GroupId, Integer> contactCounts = + new HashMap<GroupId, Integer>(); private final Map<GroupId, Integer> forumCounts = new HashMap<GroupId, Integer>(); private int contactTotal = 0, forumTotal = 0; private int nextRequestId = 0; - private ContactId activeContact; + private GroupId visibleGroup = null; private volatile Settings settings = new Settings(); @Inject public AndroidNotificationManagerImpl(DatabaseComponent db, @DatabaseExecutor Executor dbExecutor, EventBus eventBus, - Application app) { + MessagingManager messagingManager, ForumManager forumManager, + AndroidExecutor androidExecutor, Application app) { this.db = db; this.dbExecutor = dbExecutor; this.eventBus = eventBus; + this.messagingManager = messagingManager; + this.forumManager = forumManager; + this.androidExecutor = androidExecutor; appContext = app.getApplicationContext(); } @@ -104,62 +112,71 @@ EventListener { public boolean stop() { eventBus.removeListener(this); + clearNotifications(); return true; } - public void eventOccurred(Event e) { - if (e instanceof SettingsUpdatedEvent) loadSettings(); + private void clearNotifications() { + androidExecutor.execute(new Runnable() { + public void run() { + clearPrivateMessageNotification(); + clearForumPostNotification(); + } + }); } - public void showPrivateMessageNotification(ContactId c) { - lock.lock(); - try { - // check first if user has this conversation open at the moment - if (activeContact == null || !activeContact.equals(c)) { - Integer count = contactCounts.get(c); - if (count == null) contactCounts.put(c, 1); - else contactCounts.put(c, count + 1); - contactTotal++; - updatePrivateMessageNotification(); - } - } finally { - lock.unlock(); - } + private void clearPrivateMessageNotification() { + Object o = appContext.getSystemService(NOTIFICATION_SERVICE); + NotificationManager nm = (NotificationManager) o; + nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID); } - public void clearPrivateMessageNotification(ContactId c) { - lock.lock(); - try { - Integer count = contactCounts.remove(c); - if (count == null) return; // Already cleared - contactTotal -= count; - updatePrivateMessageNotification(); - } finally { - lock.unlock(); - } + private void clearForumPostNotification() { + Object o = appContext.getSystemService(NOTIFICATION_SERVICE); + NotificationManager nm = (NotificationManager) o; + nm.cancel(FORUM_POST_NOTIFICATION_ID); } - public void blockPrivateMessageNotification(ContactId c) { - lock.lock(); - try { - activeContact = c; - } finally { - lock.unlock(); + public void eventOccurred(Event e) { + if (e instanceof SettingsUpdatedEvent) { + loadSettings(); + } else if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + if (m.isValid() && !m.isLocal()) { + ClientId c = m.getClientId(); + if (c.equals(messagingManager.getClientId())) + showPrivateMessageNotification(m.getMessage().getGroupId()); + else if (c.equals(forumManager.getClientId())) + showForumPostNotification(m.getMessage().getGroupId()); + } } } - public void unblockPrivateMessageNotification(ContactId c) { - lock.lock(); - try { - if (activeContact != null && activeContact.equals(c)) { - activeContact = null; + public void showPrivateMessageNotification(final GroupId g) { + androidExecutor.execute(new Runnable() { + public void run() { + Integer count = contactCounts.get(g); + if (count == null) contactCounts.put(g, 1); + else contactCounts.put(g, count + 1); + contactTotal++; + if (!g.equals(visibleGroup)) + updatePrivateMessageNotification(); } - } finally { - lock.unlock(); - } + }); + } + + public void clearPrivateMessageNotification(final GroupId g) { + androidExecutor.execute(new Runnable() { + public void run() { + Integer count = contactCounts.remove(g); + if (count == null) return; // Already cleared + contactTotal -= count; + // FIXME: If the notification isn't showing, this may show it + updatePrivateMessageNotification(); + } + }); } - // Locking: lock private void updatePrivateMessageNotification() { if (contactTotal == 0) { clearPrivateMessageNotification(); @@ -180,9 +197,10 @@ EventListener { b.setAutoCancel(true); if (contactCounts.size() == 1) { Intent i = new Intent(appContext, ConversationActivity.class); - ContactId c = contactCounts.keySet().iterator().next(); - i.putExtra("briar.CONTACT_ID", c.getInt()); - i.setData(Uri.parse(CONTACT_URI + "/" + c.getInt())); + GroupId g = contactCounts.keySet().iterator().next(); + i.putExtra("briar.GROUP_ID", g.getBytes()); + String idHex = StringUtils.toHexString(g.getBytes()); + i.setData(Uri.parse(CONTACT_URI + "/" + idHex)); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP); TaskStackBuilder t = TaskStackBuilder.create(appContext); t.addParentStack(ConversationActivity.class); @@ -202,13 +220,6 @@ EventListener { } } - // Locking: lock - private void clearPrivateMessageNotification() { - Object o = appContext.getSystemService(NOTIFICATION_SERVICE); - NotificationManager nm = (NotificationManager) o; - nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID); - } - private int getDefaults() { int defaults = DEFAULT_LIGHTS; boolean sound = settings.getBoolean("notifySound", true); @@ -220,32 +231,31 @@ EventListener { return defaults; } - public void showForumPostNotification(GroupId g) { - lock.lock(); - try { - Integer count = forumCounts.get(g); - if (count == null) forumCounts.put(g, 1); - else forumCounts.put(g, count + 1); - forumTotal++; - updateForumPostNotification(); - } finally { - lock.unlock(); - } + public void showForumPostNotification(final GroupId g) { + androidExecutor.execute(new Runnable() { + public void run() { + Integer count = forumCounts.get(g); + if (count == null) forumCounts.put(g, 1); + else forumCounts.put(g, count + 1); + forumTotal++; + if (!g.equals(visibleGroup)) + updateForumPostNotification(); + } + }); } - public void clearForumPostNotification(GroupId g) { - lock.lock(); - try { - Integer count = forumCounts.remove(g); - if (count == null) return; // Already cleared - forumTotal -= count; - updateForumPostNotification(); - } finally { - lock.unlock(); - } + public void clearForumPostNotification(final GroupId g) { + androidExecutor.execute(new Runnable() { + public void run() { + Integer count = forumCounts.remove(g); + if (count == null) return; // Already cleared + forumTotal -= count; + // FIXME: If the notification isn't showing, this may show it + updateForumPostNotification(); + } + }); } - // Locking: lock private void updateForumPostNotification() { if (forumTotal == 0) { clearForumPostNotification(); @@ -288,23 +298,19 @@ EventListener { } } - // Locking: lock - private void clearForumPostNotification() { - Object o = appContext.getSystemService(NOTIFICATION_SERVICE); - NotificationManager nm = (NotificationManager) o; - nm.cancel(FORUM_POST_NOTIFICATION_ID); + public void blockNotification(final GroupId g) { + androidExecutor.execute(new Runnable() { + public void run() { + visibleGroup = g; + } + }); } - public void clearNotifications() { - lock.lock(); - try { - contactCounts.clear(); - forumCounts.clear(); - contactTotal = forumTotal = 0; - clearPrivateMessageNotification(); - clearForumPostNotification(); - } finally { - lock.unlock(); - } + public void unblockNotification(final GroupId g) { + androidExecutor.execute(new Runnable() { + public void run() { + if (g.equals(visibleGroup)) visibleGroup = null; + } + }); } } diff --git a/briar-android/src/org/briarproject/android/BaseActivity.java b/briar-android/src/org/briarproject/android/BaseActivity.java index 138d3447ad744eacb2bd36b3424d56dcbc70fc4a..e8c3dd98a9fee80761e8340e5e185361402c9f5d 100644 --- a/briar-android/src/org/briarproject/android/BaseActivity.java +++ b/briar-android/src/org/briarproject/android/BaseActivity.java @@ -128,6 +128,7 @@ public abstract class BaseActivity extends AppCompatActivity return scopedObjects; } + // FIXME: Factor out prefs code so it can be used by SplashScreenActivity private SharedPreferences getSharedPrefs() { return getSharedPreferences(PREFS_DB, MODE_PRIVATE); } diff --git a/briar-android/src/org/briarproject/android/BriarService.java b/briar-android/src/org/briarproject/android/BriarService.java index 366e342df3f9a290953ba5fa3c4958c477782f1c..90f774272b27d472ed7ad096141f3d3ce2a86d5b 100644 --- a/briar-android/src/org/briarproject/android/BriarService.java +++ b/briar-android/src/org/briarproject/android/BriarService.java @@ -11,22 +11,11 @@ import android.support.v4.app.NotificationCompat; import org.briarproject.R; import org.briarproject.api.android.AndroidExecutor; -import org.briarproject.api.android.AndroidNotificationManager; -import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DatabaseConfig; -import org.briarproject.api.db.DatabaseExecutor; -import org.briarproject.api.db.DbException; -import org.briarproject.api.event.Event; -import org.briarproject.api.event.EventBus; -import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.lifecycle.LifecycleManager.StartResult; -import org.briarproject.api.messaging.MessagingManager; -import org.briarproject.api.sync.GroupId; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; @@ -34,6 +23,7 @@ import javax.inject.Inject; import roboguice.service.RoboService; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; @@ -41,7 +31,7 @@ import static java.util.logging.Level.WARNING; import static org.briarproject.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING; import static org.briarproject.api.lifecycle.LifecycleManager.StartResult.SUCCESS; -public class BriarService extends RoboService implements EventListener { +public class BriarService extends RoboService { private static final int ONGOING_NOTIFICATION_ID = 1; private static final int FAILURE_NOTIFICATION_ID = 2; @@ -53,14 +43,10 @@ public class BriarService extends RoboService implements EventListener { private final Binder binder = new BriarBinder(); @Inject private DatabaseConfig databaseConfig; - @Inject private AndroidNotificationManager notificationManager; // Fields that are accessed from background threads must be volatile @Inject private volatile LifecycleManager lifecycleManager; @Inject private volatile AndroidExecutor androidExecutor; - @Inject @DatabaseExecutor private volatile Executor dbExecutor; - @Inject private volatile MessagingManager messagingManager; - @Inject private volatile EventBus eventBus; private volatile boolean started = false; @Override @@ -95,7 +81,6 @@ public class BriarService extends RoboService implements EventListener { public void run() { StartResult result = lifecycleManager.startServices(); if (result == SUCCESS) { - eventBus.addListener(BriarService.this); started = true; } else if (result == ALREADY_RUNNING) { LOG.info("Already running"); @@ -110,25 +95,34 @@ public class BriarService extends RoboService implements EventListener { }.start(); } - private void showStartupFailureNotification(StartResult result) { - NotificationCompat.Builder b = new NotificationCompat.Builder(this); - b.setSmallIcon(android.R.drawable.stat_notify_error); - b.setContentTitle(getText(R.string.startup_failed_notification_title)); - b.setContentText(getText(R.string.startup_failed_notification_text)); - Intent i = new Intent(this, StartupFailureActivity.class); - i.setFlags(FLAG_ACTIVITY_NEW_TASK); - i.putExtra("briar.START_RESULT", result); - i.putExtra("briar.FAILURE_NOTIFICATION_ID", FAILURE_NOTIFICATION_ID); - b.setContentIntent(PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT)); - Object o = getSystemService(NOTIFICATION_SERVICE); - NotificationManager nm = (NotificationManager) o; - nm.notify(FAILURE_NOTIFICATION_ID, b.build()); - - // Bring the dashboard to the front to clear all other activities - i = new Intent(this, DashboardActivity.class); - i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP); - i.putExtra("briar.STARTUP_FAILED", true); - startActivity(i); + private void showStartupFailureNotification(final StartResult result) { + androidExecutor.execute(new Runnable() { + public void run() { + NotificationCompat.Builder b = + new NotificationCompat.Builder(BriarService.this); + b.setSmallIcon(android.R.drawable.stat_notify_error); + b.setContentTitle(getText( + R.string.startup_failed_notification_title)); + b.setContentText(getText( + R.string.startup_failed_notification_text)); + Intent i = new Intent(BriarService.this, + StartupFailureActivity.class); + i.setFlags(FLAG_ACTIVITY_NEW_TASK); + i.putExtra("briar.START_RESULT", result); + i.putExtra("briar.FAILURE_NOTIFICATION_ID", + FAILURE_NOTIFICATION_ID); + b.setContentIntent(PendingIntent.getActivity(BriarService.this, + 0, i, FLAG_UPDATE_CURRENT)); + Object o = getSystemService(NOTIFICATION_SERVICE); + NotificationManager nm = (NotificationManager) o; + nm.notify(FAILURE_NOTIFICATION_ID, b.build()); + // Bring the dashboard to the front to clear the back stack + i = new Intent(BriarService.this, DashboardActivity.class); + i.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP); + i.putExtra("briar.STARTUP_FAILED", true); + startActivity(i); + } + }); } @Override @@ -146,16 +140,11 @@ public class BriarService extends RoboService implements EventListener { super.onDestroy(); LOG.info("Destroyed"); stopForeground(true); - notificationManager.clearNotifications(); // Stop the services in a background thread new Thread() { @Override public void run() { - if (started) { - eventBus.removeListener(BriarService.this); - lifecycleManager.stopServices(); - } - androidExecutor.shutdown(); + if (started) lifecycleManager.stopServices(); } }.start(); } @@ -167,39 +156,6 @@ public class BriarService extends RoboService implements EventListener { // FIXME: Work out what to do about it } - public void eventOccurred(Event e) { - if (e instanceof MessageAddedEvent) { - MessageAddedEvent m = (MessageAddedEvent) e; - GroupId g = m.getGroupId(); - ContactId c = m.getContactId(); - if (c != null) showMessageNotification(g, c); - } - } - - private void showMessageNotification(final GroupId g, final ContactId c) { - dbExecutor.execute(new Runnable() { - public void run() { - try { - lifecycleManager.waitForDatabase(); - if (g.equals(messagingManager.getConversationId(c))) - notificationManager.showPrivateMessageNotification(c); - else notificationManager.showForumPostNotification(g); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - } catch (InterruptedException e) { - LOG.info("Interruped while waiting for database"); - Thread.currentThread().interrupt(); - } - } - }); - } - - /** Waits for the database to be opened before returning. */ - public void waitForDatabase() throws InterruptedException { - lifecycleManager.waitForDatabase(); - } - /** Waits for all services to start before returning. */ public void waitForStartup() throws InterruptedException { lifecycleManager.waitForStartup(); diff --git a/briar-android/src/org/briarproject/android/CrashReportActivity.java b/briar-android/src/org/briarproject/android/CrashReportActivity.java index e412a1c41cb7832c3a87e3116b1c7d2e6c8e999b..bd7fad0bdc7f481cc7bd5c0389c98d6349de30d6 100644 --- a/briar-android/src/org/briarproject/android/CrashReportActivity.java +++ b/briar-android/src/org/briarproject/android/CrashReportActivity.java @@ -74,12 +74,12 @@ public class CrashReportActivity extends AppCompatActivity implements OnClickLis private static final Logger LOG = Logger.getLogger(CrashReportActivity.class.getName()); - private final AndroidExecutor androidExecutor = new AndroidExecutorImpl(); + private final AndroidExecutor androidExecutor = + new AndroidExecutorImpl(getApplication()); private ScrollView scroll = null; private ListLoadingProgressBar progress = null; private LinearLayout status = null; - private ImageButton share = null; private File temp = null; private volatile String stack = null; @@ -120,7 +120,7 @@ public class CrashReportActivity extends AppCompatActivity implements OnClickLis Resources res = getResources(); int background = res.getColor(R.color.button_bar_background); footer.setBackgroundColor(background); - share = new ImageButton(this); + ImageButton share = new ImageButton(this); share.setBackgroundResource(0); share.setImageResource(R.drawable.social_share); share.setOnClickListener(this); @@ -323,11 +323,11 @@ public class CrashReportActivity extends AppCompatActivity implements OnClickLis // Is Bluetooth available? BluetoothAdapter bt = null; try { - bt = androidExecutor.call(new Callable<BluetoothAdapter>() { + bt = androidExecutor.submit(new Callable<BluetoothAdapter>() { public BluetoothAdapter call() throws Exception { return BluetoothAdapter.getDefaultAdapter(); } - }); + }).get(); } catch (InterruptedException e) { LOG.warning("Interrupted while getting BluetoothAdapter"); Thread.currentThread().interrupt(); diff --git a/briar-android/src/org/briarproject/android/SplashScreenActivity.java b/briar-android/src/org/briarproject/android/SplashScreenActivity.java index ffc926b1cfaf30dfa610c2a0f6f43dc58ef6276c..ddf3dbc23fc2f6457703f0d2231118b5c11c3a3b 100644 --- a/briar-android/src/org/briarproject/android/SplashScreenActivity.java +++ b/briar-android/src/org/briarproject/android/SplashScreenActivity.java @@ -16,8 +16,8 @@ import com.google.inject.Injector; import org.briarproject.R; import org.briarproject.android.util.LayoutUtils; import org.briarproject.api.db.DatabaseConfig; +import org.briarproject.util.FileUtils; -import java.io.File; import java.util.logging.Logger; import roboguice.RoboGuice; @@ -86,8 +86,9 @@ public class SplashScreenActivity extends RoboSplashActivity { if (hex != null && databaseConfig.databaseExists()) { startActivity(new Intent(this, DashboardActivity.class)); } else { - prefs.edit().clear().commit(); - delete(databaseConfig.getDatabaseDirectory()); + prefs.edit().clear().apply(); + FileUtils.deleteFileOrDir( + databaseConfig.getDatabaseDirectory()); startActivity(new Intent(this, SetupActivity.class)); } } @@ -106,17 +107,11 @@ public class SplashScreenActivity extends RoboSplashActivity { } } - private void delete(File f) { - if (f.isFile()) f.delete(); - else if (f.isDirectory()) for (File child : f.listFiles()) delete(child); - } - private void setPreferencesDefaults() { new Thread() { @Override public void run() { - PreferenceManager - .setDefaultValues(SplashScreenActivity.this, + PreferenceManager.setDefaultValues(SplashScreenActivity.this, R.xml.panic_preferences, false); } }.start(); diff --git a/briar-android/src/org/briarproject/android/TestingActivity.java b/briar-android/src/org/briarproject/android/TestingActivity.java index fb9b7fb912532e0ed0487864010a1156375ed5a0..c21d2076992de5d46287f3b3c43dc890050c7e0f 100644 --- a/briar-android/src/org/briarproject/android/TestingActivity.java +++ b/briar-android/src/org/briarproject/android/TestingActivity.java @@ -326,11 +326,11 @@ public class TestingActivity extends BriarActivity implements OnClickListener { // Is Bluetooth available? BluetoothAdapter bt = null; try { - bt = androidExecutor.call(new Callable<BluetoothAdapter>() { + bt = androidExecutor.submit(new Callable<BluetoothAdapter>() { public BluetoothAdapter call() throws Exception { return BluetoothAdapter.getDefaultAdapter(); } - }); + }).get(); } catch (InterruptedException e) { LOG.warning("Interrupted while getting BluetoothAdapter"); Thread.currentThread().interrupt(); diff --git a/briar-android/src/org/briarproject/android/contact/ContactListActivity.java b/briar-android/src/org/briarproject/android/contact/ContactListActivity.java index 6817730efad203890c0345c61437090378dce9bd..0d5e9005d2b54d57cc05da816417ff0d0506ba5c 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListActivity.java @@ -3,7 +3,6 @@ package org.briarproject.android.contact; import android.content.Intent; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; -import android.support.v7.util.SortedList; import android.support.v7.widget.LinearLayoutManager; import android.view.View; @@ -23,10 +22,11 @@ import org.briarproject.api.event.ContactRemovedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.MessageAddedEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.plugins.ConnectionRegistry; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; import java.util.ArrayList; @@ -36,6 +36,7 @@ import java.util.logging.Logger; import javax.inject.Inject; +import static android.support.v7.util.SortedList.INVALID_POSITION; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; @@ -105,18 +106,16 @@ public class ContactListActivity extends BriarActivity for (Contact c : contactManager.getContacts()) { try { ContactId id = c.getId(); - GroupId conversation = + GroupId groupId = messagingManager.getConversationId(id); Collection<PrivateMessageHeader> headers = messagingManager.getMessageHeaders(id); - boolean connected = connectionRegistry.isConnected(c.getId()); contacts.add(new ContactListItem(c, connected, - conversation, - headers)); + groupId, headers)); } catch (NoSuchContactException e) { - // Continue + LOG.info("Contact removed"); } } displayContacts(contacts); @@ -141,7 +140,7 @@ public class ContactListActivity extends BriarActivity // sorting criteria and cause duplicates for (ContactListItem contact : contacts) { int position = adapter.findItemPosition(contact); - if (position == SortedList.INVALID_POSITION) { + if (position == INVALID_POSITION) { adapter.add(contact); } else { adapter.updateItem(position, contact); @@ -169,19 +168,22 @@ public class ContactListActivity extends BriarActivity } else if (e instanceof ContactRemovedEvent) { LOG.info("Contact removed"); removeItem(((ContactRemovedEvent) e).getContactId()); - } else if (e instanceof MessageAddedEvent) { - LOG.info("Message added, reloading"); - ContactId source = ((MessageAddedEvent) e).getContactId(); - if (source == null) loadContacts(); - else reloadContact(source); + } else if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + ClientId c = m.getClientId(); + if (m.isValid() && c.equals(messagingManager.getClientId())) { + LOG.info("Message added, reloading"); + reloadConversation(m.getMessage().getGroupId()); + } } } - private void reloadContact(final ContactId c) { + private void reloadConversation(final GroupId g) { runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); + ContactId c = messagingManager.getContactId(g); Collection<PrivateMessageHeader> headers = messagingManager.getMessageHeaders(c); long duration = System.currentTimeMillis() - now; @@ -189,7 +191,7 @@ public class ContactListActivity extends BriarActivity LOG.info("Partial load took " + duration + " ms"); updateItem(c, headers); } catch (NoSuchContactException e) { - removeItem(c); + LOG.info("Contact removed"); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java index 970461d7088740c284a4e69016f907298bcb35b4..777349e687a630cc23e67cc9366de7998d6a3e28 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java @@ -14,11 +14,12 @@ import android.widget.TextView; import org.briarproject.R; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.identity.AuthorId; import org.briarproject.api.sync.GroupId; import java.util.List; +import static android.support.v7.util.SortedList.INVALID_POSITION; + public class ContactListAdapter extends RecyclerView.Adapter<ContactListAdapter.ContactHolder> { @@ -130,17 +131,9 @@ public class ContactListAdapter ui.layout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - ContactId contactId = item.getContact().getId(); - String contactName = item.getContact().getAuthor().getName(); - GroupId groupId = item.getConversationId(); - AuthorId localAuthorId = item.getContact().getLocalAuthorId(); - + GroupId groupId = item.getGroupId(); Intent i = new Intent(ctx, ConversationActivity.class); - i.putExtra("briar.CONTACT_ID", contactId.getInt()); - i.putExtra("briar.CONTACT_NAME", contactName); i.putExtra("briar.GROUP_ID", groupId.getBytes()); - i.putExtra("briar.LOCAL_AUTHOR_ID", localAuthorId.getBytes()); - ctx.startActivity(i); } }); @@ -156,8 +149,7 @@ public class ContactListAdapter } public ContactListItem getItem(int position) { - if (position == SortedList.INVALID_POSITION || - contacts.size() <= position) { + if (position == INVALID_POSITION || contacts.size() <= position) { return null; // Not found } return contacts.get(position); @@ -186,7 +178,7 @@ public class ContactListAdapter ContactListItem item = getItem(i); if (item.getContact().getId().equals(c)) return i; } - return SortedList.INVALID_POSITION; // Not found + return INVALID_POSITION; // Not found } public void addAll(final List<ContactListItem> contacts) { diff --git a/briar-android/src/org/briarproject/android/contact/ContactListItem.java b/briar-android/src/org/briarproject/android/contact/ContactListItem.java index 2553c659c02e9c871115bf273a3a2eca3c97168d..2addb9e9d157a234a4554778e76099b7d7f3f636 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListItem.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListItem.java @@ -10,15 +10,15 @@ import java.util.Collection; class ContactListItem { private final Contact contact; - private final GroupId conversation; + private final GroupId groupId; private boolean connected, empty; private long timestamp; private int unread; - ContactListItem(Contact contact, boolean connected, GroupId conversation, + ContactListItem(Contact contact, boolean connected, GroupId groupId, Collection<PrivateMessageHeader> headers) { this.contact = contact; - this.conversation = conversation; + this.groupId = groupId; this.connected = connected; setHeaders(headers); } @@ -39,8 +39,8 @@ class ContactListItem { return contact; } - GroupId getConversationId() { - return conversation; + GroupId getGroupId() { + return groupId; } boolean isConnected() { diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index 18f24f9c992811be40f3a5eed8c7e016ea31ec1f..6b96389c8c8fbac2f62353b5c6c219fa5d618a4e 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -27,21 +27,19 @@ import org.briarproject.api.crypto.CryptoExecutor; import org.briarproject.api.db.DbException; import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.db.NoSuchMessageException; -import org.briarproject.api.db.NoSuchSubscriptionException; import org.briarproject.api.event.ContactConnectedEvent; import org.briarproject.api.event.ContactDisconnectedEvent; import org.briarproject.api.event.ContactRemovedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.MessageAddedEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.messaging.MessagingManager; -import org.briarproject.api.messaging.PrivateConversation; +import org.briarproject.api.messaging.PrivateMessage; import org.briarproject.api.messaging.PrivateMessageFactory; import org.briarproject.api.messaging.PrivateMessageHeader; -import org.briarproject.api.messaging.PrivateMessageHeader.Status; import org.briarproject.api.plugins.ConnectionRegistry; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; @@ -66,8 +64,6 @@ import javax.inject.Inject; import static android.widget.Toast.LENGTH_SHORT; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; -import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED; -import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT; public class ConversationActivity extends BriarActivity implements EventListener, OnClickListener { @@ -89,20 +85,19 @@ public class ConversationActivity extends BriarActivity @Inject private volatile MessagingManager messagingManager; @Inject private volatile EventBus eventBus; @Inject private volatile PrivateMessageFactory privateMessageFactory; + private volatile GroupId groupId = null; private volatile ContactId contactId = null; private volatile String contactName = null; - private volatile GroupId groupId = null; - private volatile PrivateConversation conversation = null; - private volatile boolean connected; + private volatile boolean connected = false; @Override public void onCreate(Bundle state) { super.onCreate(state); Intent i = getIntent(); - int id = i.getIntExtra("briar.CONTACT_ID", -1); - if (id == -1) throw new IllegalStateException(); - contactId = new ContactId(id); + byte[] b = i.getByteArrayExtra("briar.GROUP_ID"); + if (b == null) throw new IllegalStateException(); + groupId = new GroupId(b); setContentView(R.layout.activity_conversation); @@ -122,19 +117,17 @@ public class ConversationActivity extends BriarActivity public void onResume() { super.onResume(); eventBus.addListener(this); - notificationManager.blockPrivateMessageNotification(contactId); - loadContactAndGroup(); + notificationManager.blockNotification(groupId); + notificationManager.clearPrivateMessageNotification(groupId); + loadContactDetails(); loadHeaders(); - - // remove the notification for this conversation since we see it now - notificationManager.clearPrivateMessageNotification(contactId); } @Override public void onPause() { super.onPause(); eventBus.removeListener(this); - notificationManager.unblockPrivateMessageNotification(contactId); + notificationManager.unblockNotification(groupId); if (isFinishing()) markMessagesRead(); } @@ -164,26 +157,21 @@ public class ConversationActivity extends BriarActivity } } - private void loadContactAndGroup() { + private void loadContactDetails() { runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); + contactId = messagingManager.getContactId(groupId); Contact contact = contactManager.getContact(contactId); contactName = contact.getAuthor().getName(); - groupId = messagingManager.getConversationId(contactId); - conversation = messagingManager.getConversation(groupId); connected = connectionRegistry.isConnected(contactId); long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) { - LOG.info("Loading contact and conversation took " - + duration + " ms"); - } + if (LOG.isLoggable(INFO)) + LOG.info("Loading contact took " + duration + " ms"); displayContactDetails(); } catch (NoSuchContactException e) { finishOnUiThread(); - } catch (NoSuchSubscriptionException e) { - finishOnUiThread(); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -234,7 +222,11 @@ public class ConversationActivity extends BriarActivity runOnUiThread(new Runnable() { public void run() { sendButton.setEnabled(true); - if (!headers.isEmpty()) { + if (headers.isEmpty()) { + // we have no messages, + // so let the list know to hide progress bar + list.showData(); + } else { for (PrivateMessageHeader h : headers) { ConversationItem item = new ConversationItem(h); byte[] body = bodyCache.get(h.getId()); @@ -244,10 +236,6 @@ public class ConversationActivity extends BriarActivity } // Scroll to the bottom list.scrollToPosition(adapter.getItemCount() - 1); - } else { - // we have no messages, - // so let the list know to hide progress bar - list.showData(); } } }); @@ -278,17 +266,13 @@ public class ConversationActivity extends BriarActivity public void run() { bodyCache.put(m, body); int count = adapter.getItemCount(); - for (int i = 0; i < count; i++) { ConversationItem item = adapter.getItem(i); - if (item.getHeader().getId().equals(m)) { item.setBody(body); adapter.notifyItemChanged(i); - // Scroll to the bottom list.scrollToPosition(count - 1); - return; } } @@ -297,7 +281,6 @@ public class ConversationActivity extends BriarActivity } private void markMessagesRead() { - notificationManager.clearPrivateMessageNotification(contactId); List<MessageId> unread = new ArrayList<MessageId>(); int count = adapter.getItemCount(); for (int i = 0; i < count; i++) { @@ -335,35 +318,25 @@ public class ConversationActivity extends BriarActivity LOG.info("Contact removed"); finishOnUiThread(); } - } else if (e instanceof MessageAddedEvent) { - MessageAddedEvent mEvent = (MessageAddedEvent) e; - GroupId g = mEvent.getGroupId(); - if (g.equals(groupId)) { - // mark new incoming messages as read directly - if (mEvent.getContactId() != null) { - ConversationItem item = adapter.getLastItem(); - if (item != null) { - markIncomingMessageRead(mEvent.getMessage(), - item.getHeader().getTimestamp()); - } - } - + } else if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { LOG.info("Message added, reloading"); - // TODO: get and add the ConversationItem here to prevent - // reloading the entire conversation - loadHeaders(); + // Mark new incoming messages as read directly + if (m.isLocal()) loadHeaders(); + else markMessageReadIfNew(m.getMessage()); } } else if (e instanceof MessagesSentEvent) { MessagesSentEvent m = (MessagesSentEvent) e; if (m.getContactId().equals(contactId)) { LOG.info("Messages sent"); - markMessages(m.getMessageIds(), SENT); + markMessages(m.getMessageIds(), true, false); } } else if (e instanceof MessagesAckedEvent) { MessagesAckedEvent m = (MessagesAckedEvent) e; if (m.getContactId().equals(contactId)) { LOG.info("Messages acked"); - markMessages(m.getMessageIds(), DELIVERED); + markMessages(m.getMessageIds(), true, true); } } else if (e instanceof ContactConnectedEvent) { ContactConnectedEvent c = (ContactConnectedEvent) e; @@ -382,58 +355,55 @@ public class ConversationActivity extends BriarActivity } } - private void markMessages(final Collection<MessageId> messageIds, - final Status status) { + private void markMessageReadIfNew(final Message m) { runOnUiThread(new Runnable() { public void run() { - Set<MessageId> messages = new HashSet<MessageId>(messageIds); - int count = adapter.getItemCount(); - for (int i = 0; i < count; i++) { - ConversationItem item = adapter.getItem(i); - if (messages.contains(item.getHeader().getId())) { - item.setStatus(status); - adapter.notifyItemChanged(i); - } + ConversationItem item = adapter.getLastItem(); + if (item != null) { + // Mark the message read if it's the newest message + long lastMsgTime = item.getHeader().getTimestamp(); + long newMsgTime = m.getTimestamp(); + if (newMsgTime > lastMsgTime) markNewMessageRead(m); + else loadHeaders(); } } }); } - private void markIncomingMessageRead(final Message m, - final long lastMsgTime) { - - // stop here if message is older than latest message we have - long newMsgTime = m.getTimestamp(); - if (newMsgTime < lastMsgTime) return; - + private void markNewMessageRead(final Message m) { runOnDbThread(new Runnable() { public void run() { try { - // mark messages as read, because is latest messagingManager.setReadFlag(m.getId(), true); - showIncomingMessageRead(); + loadHeaders(); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); } } - // TODO else: smooth-scroll up to unread messages if out of view }); } - private void showIncomingMessageRead() { + private void markMessages(final Collection<MessageId> messageIds, + final boolean sent, final boolean seen) { runOnUiThread(new Runnable() { public void run() { - // this is only called from markIncomingMessageRead() - // so we can assume that it was the last message that changed - adapter.notifyItemChanged(adapter.getItemCount() - 1); + Set<MessageId> messages = new HashSet<MessageId>(messageIds); + int count = adapter.getItemCount(); + for (int i = 0; i < count; i++) { + ConversationItem item = adapter.getItem(i); + if (messages.contains(item.getHeader().getId())) { + item.setSent(sent); + item.setSeen(seen); + adapter.notifyItemChanged(i); + } + } } }); } public void onClick(View view) { markMessagesRead(); - String message = content.getText().toString(); if (message.equals("")) return; long timestamp = System.currentTimeMillis(); @@ -445,21 +415,16 @@ public class ConversationActivity extends BriarActivity private long getMinTimestampForNewMessage() { // Don't use an earlier timestamp than the newest message - long timestamp = 0; ConversationItem item = adapter.getLastItem(); - if (item != null) { - timestamp = item.getHeader().getTimestamp(); - } - return timestamp + 1; + return item == null ? 0 : item.getHeader().getTimestamp() + 1; } private void createMessage(final byte[] body, final long timestamp) { cryptoExecutor.execute(new Runnable() { public void run() { try { - Message m = privateMessageFactory.createPrivateMessage(null, - conversation, "text/plain", timestamp, body); - storeMessage(m); + storeMessage(privateMessageFactory.createPrivateMessage( + groupId, timestamp, null, "text/plain", body)); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } catch (IOException e) { @@ -469,7 +434,7 @@ public class ConversationActivity extends BriarActivity }); } - private void storeMessage(final Message m) { + private void storeMessage(final PrivateMessage m) { runOnDbThread(new Runnable() { public void run() { try { @@ -487,29 +452,20 @@ public class ConversationActivity extends BriarActivity } private void askToRemoveContact() { - runOnUiThread(new Runnable() { - @Override - public void run() { - DialogInterface.OnClickListener okListener = - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - removeContact(); - } - }; - - AlertDialog.Builder builder = - new AlertDialog.Builder(ConversationActivity.this); - builder.setTitle( - getString(R.string.dialog_title_delete_contact)); - builder.setMessage( - getString(R.string.dialog_message_delete_contact)); - builder.setPositiveButton(android.R.string.ok, okListener); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } - }); + DialogInterface.OnClickListener okListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + removeContact(); + } + }; + AlertDialog.Builder builder = + new AlertDialog.Builder(ConversationActivity.this); + builder.setTitle(getString(R.string.dialog_title_delete_contact)); + builder.setMessage(getString(R.string.dialog_message_delete_contact)); + builder.setPositiveButton(android.R.string.ok, okListener); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } private void removeContact() { diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java index 6b0c1aac62fd50392a13840f824b10edc6ccb768..96f61a4499b1cea9fb2944a71177376296cd4ce4 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java @@ -14,9 +14,6 @@ import org.briarproject.R; import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.util.StringUtils; -import static org.briarproject.api.messaging.PrivateMessageHeader.Status.DELIVERED; -import static org.briarproject.api.messaging.PrivateMessageHeader.Status.SENT; - class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapter.MessageHolder> { @@ -113,18 +110,18 @@ class ConversationAdapter extends PrivateMessageHeader header = item.getHeader(); if (header.isLocal()) { - if (item.getStatus() == DELIVERED) { + if (item.isSeen()) { ui.status.setImageResource(R.drawable.message_delivered); - } else if (item.getStatus() == SENT) { + } else if (item.isSent()) { ui.status.setImageResource(R.drawable.message_sent); } else { ui.status.setImageResource(R.drawable.message_stored); } } else if (!header.isRead()) { - int bottom = ui.layout.getPaddingBottom(); + int left = ui.layout.getPaddingLeft(); int top = ui.layout.getPaddingTop(); int right = ui.layout.getPaddingRight(); - int left = ui.layout.getPaddingLeft(); + int bottom = ui.layout.getPaddingBottom(); // show unread messages in different color to not miss them ui.layout.setBackgroundResource(R.drawable.msg_in_unread); @@ -185,7 +182,9 @@ class ConversationAdapter extends this.messages.endBatchedUpdates(); } + // TODO: Does this class need to be public? public static class MessageHolder extends RecyclerView.ViewHolder { + public ViewGroup layout; public TextView body; public TextView date; diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java index 7ee6c3bd7d2bd61d00291aac18472506a77e6fd6..7603c5c8f19f0ee4dd064f616b3a737007ff3de5 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java @@ -1,19 +1,19 @@ package org.briarproject.android.contact; import org.briarproject.api.messaging.PrivateMessageHeader; -import org.briarproject.api.messaging.PrivateMessageHeader.Status; // This class is not thread-safe class ConversationItem { private final PrivateMessageHeader header; private byte[] body; - private Status status; + private boolean sent, seen; ConversationItem(PrivateMessageHeader header) { this.header = header; body = null; - status = header.getStatus(); + sent = header.isSent(); + seen = header.isSeen(); } PrivateMessageHeader getHeader() { @@ -28,11 +28,19 @@ class ConversationItem { this.body = body; } - Status getStatus() { - return status; + boolean isSent() { + return sent; } - void setStatus(Status status) { - this.status = status; + void setSent(boolean sent) { + this.sent = sent; + } + + boolean isSeen() { + return seen; + } + + void setSeen(boolean seen) { + this.seen = seen; } } diff --git a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java index 5fc968a961b66ced3bd4d6116fde70dbba19f07b..8ae263061408e2be827da4f176c3e0ef32de08a6 100644 --- a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java @@ -18,7 +18,6 @@ import org.briarproject.android.BriarActivity; import org.briarproject.android.util.LayoutUtils; import org.briarproject.api.db.DbException; import org.briarproject.api.forum.Forum; -import org.briarproject.api.forum.ForumFactory; import org.briarproject.api.forum.ForumManager; import org.briarproject.util.StringUtils; @@ -38,7 +37,7 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH; import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP; -import static org.briarproject.api.sync.MessagingConstants.MAX_GROUP_NAME_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; public class CreateForumActivity extends BriarActivity implements OnEditorActionListener, OnClickListener { @@ -52,7 +51,6 @@ implements OnEditorActionListener, OnClickListener { private TextView feedback = null; // Fields that are accessed from background threads must be volatile - @Inject private volatile ForumFactory forumFactory; @Inject private volatile ForumManager forumManager; @Override @@ -115,8 +113,9 @@ implements OnEditorActionListener, OnClickListener { } private boolean validateName() { - int length = StringUtils.toUtf8(nameEntry.getText().toString()).length; - if (length > MAX_GROUP_NAME_LENGTH) { + String name = nameEntry.getText().toString(); + int length = StringUtils.toUtf8(name).length; + if (length > MAX_FORUM_NAME_LENGTH) { feedback.setText(R.string.name_too_long); return false; } @@ -138,8 +137,8 @@ implements OnEditorActionListener, OnClickListener { runOnDbThread(new Runnable() { public void run() { try { - Forum f = forumFactory.createForum(name); long now = System.currentTimeMillis(); + Forum f = forumManager.createForum(name); forumManager.addForum(f); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index 2916238f9a604b4d9cc15ea6e947964ce932dc8e..73706fad8997f20e07a834fd08073e65252f676b 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -24,7 +24,7 @@ import org.briarproject.api.db.NoSuchSubscriptionException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.MessageAddedEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.SubscriptionRemovedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; @@ -56,7 +56,7 @@ import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1; public class ForumActivity extends BriarActivity implements EventListener, -OnClickListener, OnItemClickListener { + OnClickListener, OnItemClickListener { private static final int REQUEST_READ = 2; private static final Logger LOG = @@ -143,6 +143,8 @@ OnClickListener, OnItemClickListener { public void onResume() { super.onResume(); eventBus.addListener(this); + notificationManager.blockNotification(groupId); + notificationManager.clearForumPostNotification(groupId); loadForum(); loadHeaders(); } @@ -276,11 +278,11 @@ OnClickListener, OnItemClickListener { public void onPause() { super.onPause(); eventBus.removeListener(this); + notificationManager.unblockNotification(groupId); if (isFinishing()) markPostsRead(); } private void markPostsRead() { - notificationManager.clearForumPostNotification(groupId); List<MessageId> unread = new ArrayList<MessageId>(); int count = adapter.getCount(); for (int i = 0; i < count; i++) { @@ -312,8 +314,9 @@ OnClickListener, OnItemClickListener { } public void eventOccurred(Event e) { - if (e instanceof MessageAddedEvent) { - if (((MessageAddedEvent) e).getGroupId().equals(groupId)) { + if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { LOG.info("Message added, reloading"); loadHeaders(); } diff --git a/briar-android/src/org/briarproject/android/forum/ForumListActivity.java b/briar-android/src/org/briarproject/android/forum/ForumListActivity.java index fe1a08b7cde757d9c89799cc53ebcd6cd79bde1d..7ee1693a498cc0afc830626216b7f73dab18cd2d 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListActivity.java @@ -28,19 +28,18 @@ import org.briarproject.api.db.NoSuchSubscriptionException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; -import org.briarproject.api.event.MessageAddedEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.SubscriptionAddedEvent; import org.briarproject.api.event.SubscriptionRemovedEvent; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostHeader; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import javax.inject.Inject; @@ -60,16 +59,13 @@ import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP; import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1; public class ForumListActivity extends BriarActivity -implements EventListener, OnClickListener, OnItemClickListener, -OnCreateContextMenuListener { + implements EventListener, OnClickListener, OnItemClickListener, + OnCreateContextMenuListener { private static final int MENU_ITEM_UNSUBSCRIBE = 1; private static final Logger LOG = Logger.getLogger(ForumListActivity.class.getName()); - private final Map<GroupId, GroupId> groupIds = - new ConcurrentHashMap<GroupId, GroupId>(); - private TextView empty = null; private ForumListAdapter adapter = null; private ListView list = null; @@ -179,7 +175,6 @@ OnCreateContextMenuListener { private void clearHeaders() { runOnUiThread(new Runnable() { public void run() { - groupIds.clear(); empty.setVisibility(GONE); list.setVisibility(GONE); available.setVisibility(GONE); @@ -194,12 +189,10 @@ OnCreateContextMenuListener { final Collection<ForumPostHeader> headers) { runOnUiThread(new Runnable() { public void run() { - GroupId id = f.getId(); - groupIds.put(id, id); list.setVisibility(VISIBLE); loading.setVisibility(GONE); // Remove the old item, if any - ForumListItem item = findForum(id); + ForumListItem item = findForum(f.getId()); if (item != null) adapter.remove(item); // Add a new item adapter.add(new ForumListItem(f, headers)); @@ -255,11 +248,12 @@ OnCreateContextMenuListener { } public void eventOccurred(Event e) { - if (e instanceof MessageAddedEvent) { - GroupId g = ((MessageAddedEvent) e).getGroupId(); - if (groupIds.containsKey(g)) { + if (e instanceof MessageValidatedEvent) { + MessageValidatedEvent m = (MessageValidatedEvent) e; + ClientId c = m.getClientId(); + if (m.isValid() && c.equals(forumManager.getClientId())) { LOG.info("Message added, reloading"); - loadHeaders(g); + loadHeaders(m.getMessage().getGroupId()); } } else if (e instanceof RemoteSubscriptionsUpdatedEvent) { LOG.info("Remote subscriptions changed, reloading"); @@ -269,7 +263,7 @@ OnCreateContextMenuListener { loadHeaders(); } else if (e instanceof SubscriptionRemovedEvent) { Group g = ((SubscriptionRemovedEvent) e).getGroup(); - if (groupIds.containsKey(g.getId())) { + if (g.getClientId().equals(forumManager.getClientId())) { LOG.info("Group removed, reloading"); loadHeaders(); } @@ -303,7 +297,6 @@ OnCreateContextMenuListener { public void run() { ForumListItem item = findForum(g); if (item != null) { - groupIds.remove(g); adapter.remove(item); adapter.notifyDataSetChanged(); if (adapter.isEmpty()) { diff --git a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java index c3346813c5d010d81ff9ef459d25654b2c32fcde..a29fade64a183f139cc3e76dca1d3abf7022feb7 100644 --- a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java +++ b/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java @@ -30,12 +30,12 @@ import org.briarproject.api.crypto.PrivateKey; import org.briarproject.api.db.DbException; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumPost; import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.sync.GroupId; -import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.util.StringUtils; @@ -267,35 +267,35 @@ implements OnItemSelectedListener, OnClickListener { // Don't use an earlier timestamp than the newest post long timestamp = System.currentTimeMillis(); timestamp = Math.max(timestamp, minTimestamp); - Message m; + ForumPost p; try { if (localAuthor == null) { - m = forumPostFactory.createAnonymousPost(parentId, - forum, "text/plain", timestamp, body); + p = forumPostFactory.createAnonymousPost(groupId, + timestamp, parentId, "text/plain", body); } else { KeyParser keyParser = crypto.getSignatureKeyParser(); byte[] b = localAuthor.getPrivateKey(); PrivateKey authorKey = keyParser.parsePrivateKey(b); - m = forumPostFactory.createPseudonymousPost(parentId, - forum, localAuthor, authorKey, "text/plain", - timestamp, body); + p = forumPostFactory.createPseudonymousPost(groupId, + timestamp, parentId, localAuthor, "text/plain", + body, authorKey); } } catch (GeneralSecurityException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } - storePost(m); + storePost(p); } }); } - private void storePost(final Message m) { + private void storePost(final ForumPost p) { runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); - forumManager.addLocalPost(m); + forumManager.addLocalPost(p); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Storing message took " + duration + " ms"); diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java index ae5d0dd409779caa0ef198ec9995dcb71b1d9422..0d622ef7791e050dcd03f0d828c5d8193a1b7dfa 100644 --- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java +++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java @@ -110,11 +110,11 @@ class DroidtoothPlugin implements DuplexPlugin { // BluetoothAdapter.getDefaultAdapter() must be called on a thread // with a message queue, so submit it to the AndroidExecutor try { - adapter = androidExecutor.call(new Callable<BluetoothAdapter>() { + adapter = androidExecutor.submit(new Callable<BluetoothAdapter>() { public BluetoothAdapter call() throws Exception { return BluetoothAdapter.getDefaultAdapter(); } - }); + }).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while getting BluetoothAdapter"); diff --git a/briar-api/src/org/briarproject/api/UniqueId.java b/briar-api/src/org/briarproject/api/UniqueId.java index 0c690ff2fd567ab4c7848fa625ea4a03f048697a..9602a5fa4d98a65379e3f3c9d776ee69484e6a06 100644 --- a/briar-api/src/org/briarproject/api/UniqueId.java +++ b/briar-api/src/org/briarproject/api/UniqueId.java @@ -1,6 +1,7 @@ package org.briarproject.api; import java.util.Arrays; +import java.util.Comparator; public abstract class UniqueId { @@ -27,4 +28,20 @@ public abstract class UniqueId { if (hashCode == -1) hashCode = Arrays.hashCode(id); return hashCode; } + + public static class IdComparator implements Comparator<UniqueId> { + + public static final IdComparator INSTANCE = new IdComparator(); + + @Override + public int compare(UniqueId a, UniqueId b) { + byte[] aBytes = a.getBytes(), bBytes = b.getBytes(); + for (int i = 0; i < UniqueId.LENGTH; i++) { + int aUnsigned = aBytes[i] & 0xFF, bUnsigned = bBytes[i] & 0xFF; + if (aUnsigned < bUnsigned) return -1; + if (aUnsigned > bUnsigned) return 1; + } + return 0; + } + } } diff --git a/briar-api/src/org/briarproject/api/android/AndroidExecutor.java b/briar-api/src/org/briarproject/api/android/AndroidExecutor.java index 4f964d6a12b50176f7b8033526ee201ec9c841e0..635702d0154d9a1ffe85ee8644c85dbd47071dba 100644 --- a/briar-api/src/org/briarproject/api/android/AndroidExecutor.java +++ b/briar-api/src/org/briarproject/api/android/AndroidExecutor.java @@ -1,7 +1,7 @@ package org.briarproject.api.android; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; /** * Enables background threads to make Android API calls that must be made from @@ -10,10 +10,11 @@ import java.util.concurrent.ExecutionException; public interface AndroidExecutor { /** - * Runs the given task on a thread with a message queue and returns the - * result of the task. + * Runs the given task on the main UI thread and returns a Future for + * getting the result. */ - <V> V call(Callable<V> c) throws InterruptedException, ExecutionException; + <V> Future<V> submit(Callable<V> c); - void shutdown(); + /** Runs the given task on the main UI thread. */ + void execute(Runnable r); } diff --git a/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java b/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java index d51c6e3f7b5ccf90c37c7a8e5946c4e4505b9bb7..ed3223c4b7652ec44f31e6674399efaa6de7bb97 100644 --- a/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java +++ b/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java @@ -1,26 +1,20 @@ package org.briarproject.api.android; -import org.briarproject.api.contact.ContactId; import org.briarproject.api.lifecycle.Service; import org.briarproject.api.sync.GroupId; -/** - * Manages notifications for private messages and group posts. All methods must - * be called from the Android UI thread. - */ +/** Manages notifications for private messages and forum posts. */ public interface AndroidNotificationManager extends Service { - void showPrivateMessageNotification(ContactId c); + void showPrivateMessageNotification(GroupId g); - void clearPrivateMessageNotification(ContactId c); - - void blockPrivateMessageNotification(ContactId c); - - void unblockPrivateMessageNotification(ContactId c); + void clearPrivateMessageNotification(GroupId g); void showForumPostNotification(GroupId g); void clearForumPostNotification(GroupId g); - void clearNotifications(); + void blockNotification(GroupId g); + + void unblockNotification(GroupId g); } diff --git a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java index 3a69a4e79144cddbe9158b67bc8ded4b2e866cc6..836628c906a9cd997cf300ab10711454876aaa42 100644 --- a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java +++ b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java @@ -57,9 +57,6 @@ public interface CryptoComponent { */ byte[] deriveSignatureNonce(SecretKey master, boolean alice); - /** Derives a group salt from the given master secret. */ - byte[] deriveGroupSalt(SecretKey master); - /** * Derives initial transport keys for the given transport in the given * rotation period from the given master secret. @@ -77,6 +74,12 @@ public interface CryptoComponent { /** Encodes the pseudo-random tag that is used to recognise a stream. */ void encodeTag(byte[] tag, SecretKey tagKey, long streamNumber); + /** + * Returns the hash of the given inputs. The inputs are unambiguously + * combined by prefixing each input with its length. + */ + byte[] hash(byte[]... inputs); + /** * Encrypts and authenticates the given plaintext so it can be written to * storage. The encryption and authentication keys are derived from the diff --git a/briar-api/src/org/briarproject/api/data/BdfDictionary.java b/briar-api/src/org/briarproject/api/data/BdfDictionary.java index eecea996c123ed0dfb4ca4ef42987531362b3c22..54a20f6f51b71fffb9f63e5d6b4c1c8e5430d7b3 100644 --- a/briar-api/src/org/briarproject/api/data/BdfDictionary.java +++ b/briar-api/src/org/briarproject/api/data/BdfDictionary.java @@ -1,9 +1,18 @@ package org.briarproject.api.data; -import java.util.HashMap; +import org.briarproject.api.FormatException; -// This class is not thread-safe -public class BdfDictionary extends HashMap<String, Object> { +import java.util.Hashtable; + +public class BdfDictionary extends Hashtable<String, Object> { + + public static final Object NULL_VALUE = new Object(); + + public Boolean getBoolean(String key) throws FormatException { + Object o = get(key); + if (o instanceof Boolean) return (Boolean) o; + throw new FormatException(); + } public Boolean getBoolean(String key, Boolean defaultValue) { Object o = get(key); @@ -11,36 +20,72 @@ public class BdfDictionary extends HashMap<String, Object> { return defaultValue; } + public Long getInteger(String key) throws FormatException { + Object o = get(key); + if (o instanceof Long) return (Long) o; + throw new FormatException(); + } + public Long getInteger(String key, Long defaultValue) { Object o = get(key); if (o instanceof Long) return (Long) o; return defaultValue; } + public Double getFloat(String key) throws FormatException { + Object o = get(key); + if (o instanceof Double) return (Double) o; + throw new FormatException(); + } + public Double getFloat(String key, Double defaultValue) { Object o = get(key); if (o instanceof Double) return (Double) o; return defaultValue; } + public String getString(String key) throws FormatException { + Object o = get(key); + if (o instanceof String) return (String) o; + throw new FormatException(); + } + public String getString(String key, String defaultValue) { Object o = get(key); if (o instanceof String) return (String) o; return defaultValue; } + public byte[] getRaw(String key) throws FormatException { + Object o = get(key); + if (o instanceof byte[]) return (byte[]) o; + throw new FormatException(); + } + public byte[] getRaw(String key, byte[] defaultValue) { Object o = get(key); if (o instanceof byte[]) return (byte[]) o; return defaultValue; } + public BdfList getList(String key) throws FormatException { + Object o = get(key); + if (o instanceof BdfList) return (BdfList) o; + throw new FormatException(); + } + public BdfList getList(String key, BdfList defaultValue) { Object o = get(key); if (o instanceof BdfList) return (BdfList) o; return defaultValue; } + public BdfDictionary getDictionary(String key) throws FormatException { + Object o = get(key); + if (o instanceof BdfDictionary) return (BdfDictionary) o; + throw new FormatException(); + } + public BdfDictionary getDictionary(String key, BdfDictionary defaultValue) { Object o = get(key); if (o instanceof BdfDictionary) return (BdfDictionary) o; diff --git a/briar-api/src/org/briarproject/api/data/BdfList.java b/briar-api/src/org/briarproject/api/data/BdfList.java index 949d414676c10f27d6824df29841cbeb573beb49..2caa597ec65103c4da429867c878bfa82ca0ac16 100644 --- a/briar-api/src/org/briarproject/api/data/BdfList.java +++ b/briar-api/src/org/briarproject/api/data/BdfList.java @@ -1,9 +1,16 @@ package org.briarproject.api.data; -import java.util.ArrayList; +import org.briarproject.api.FormatException; -// This class is not thread-safe -public class BdfList extends ArrayList<Object> { +import java.util.Vector; + +public class BdfList extends Vector<Object> { + + public Boolean getBoolean(int index) throws FormatException { + Object o = get(index); + if (o instanceof Boolean) return (Boolean) o; + throw new FormatException(); + } public Boolean getBoolean(int index, Boolean defaultValue) { Object o = get(index); @@ -11,36 +18,72 @@ public class BdfList extends ArrayList<Object> { return defaultValue; } + public Long getInteger(int index) throws FormatException { + Object o = get(index); + if (o instanceof Long) return (Long) o; + throw new FormatException(); + } + public Long getInteger(int index, Long defaultValue) { Object o = get(index); if (o instanceof Long) return (Long) o; return defaultValue; } + public Double getFloat(int index) throws FormatException { + Object o = get(index); + if (o instanceof Double) return (Double) o; + throw new FormatException(); + } + public Double getFloat(int index, Double defaultValue) { Object o = get(index); if (o instanceof Double) return (Double) o; return defaultValue; } + public String getString(int index) throws FormatException { + Object o = get(index); + if (o instanceof String) return (String) o; + throw new FormatException(); + } + public String getString(int index, String defaultValue) { Object o = get(index); if (o instanceof String) return (String) o; return defaultValue; } + public byte[] getRaw(int index) throws FormatException { + Object o = get(index); + if (o instanceof byte[]) return (byte[]) o; + throw new FormatException(); + } + public byte[] getRaw(int index, byte[] defaultValue) { Object o = get(index); if (o instanceof byte[]) return (byte[]) o; return defaultValue; } + public BdfList getList(int index) throws FormatException { + Object o = get(index); + if (o instanceof BdfList) return (BdfList) o; + throw new FormatException(); + } + public BdfList getList(int index, BdfList defaultValue) { Object o = get(index); if (o instanceof BdfList) return (BdfList) o; return defaultValue; } + public BdfDictionary getDictionary(int index) throws FormatException { + Object o = get(index); + if (o instanceof BdfDictionary) return (BdfDictionary) o; + throw new FormatException(); + } + public BdfDictionary getDictionary(int index, BdfDictionary defaultValue) { Object o = get(index); if (o instanceof BdfDictionary) return (BdfDictionary) o; diff --git a/briar-api/src/org/briarproject/api/data/BdfReader.java b/briar-api/src/org/briarproject/api/data/BdfReader.java index d279fe7e11b4b3b4356c9ef64b01ef9d9c145bb7..813dc86c942fbeed7e347da84e0bef9b01e6dddb 100644 --- a/briar-api/src/org/briarproject/api/data/BdfReader.java +++ b/briar-api/src/org/briarproject/api/data/BdfReader.java @@ -7,9 +7,6 @@ public interface BdfReader { boolean eof() throws IOException; void close() throws IOException; - void addConsumer(Consumer c); - void removeConsumer(Consumer c); - boolean hasNull() throws IOException; void readNull() throws IOException; void skipNull() throws IOException; diff --git a/briar-api/src/org/briarproject/api/data/DataConstants.java b/briar-api/src/org/briarproject/api/data/DataConstants.java deleted file mode 100644 index 7ab04132e8ec884e62588983c7f2203dbe0e3237..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/data/DataConstants.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.briarproject.api.data; - -import org.briarproject.api.UniqueId; - -public interface DataConstants { - - int LIST_START_LENGTH = 1; - - int LIST_END_LENGTH = 1; - - int UNIQUE_ID_LENGTH = 2 + UniqueId.LENGTH; -} diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index 086689409b835ed413541fde6dffa092d6bc0dd9..84cca32ea7d2333d80d60b4b7a4b3654ce5face5 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -9,11 +9,12 @@ import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.sync.Ack; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageHeader; import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Request; import org.briarproject.api.sync.SubscriptionAck; @@ -44,9 +45,12 @@ public interface DatabaseComponent { */ ContactId addContact(Author remote, AuthorId local) throws DbException; + /** Adds a group to the given contact's subscriptions. */ + void addContactGroup(ContactId c, Group g) throws DbException; + /** * Subscribes to a group, or returns false if the user already has the - * maximum number of public subscriptions. + * maximum number of subscriptions. */ boolean addGroup(Group g) throws DbException; @@ -54,7 +58,8 @@ public interface DatabaseComponent { void addLocalAuthor(LocalAuthor a) throws DbException; /** Stores a local message. */ - void addLocalMessage(Message m) throws DbException; + void addLocalMessage(Message m, ClientId c, Metadata meta) + throws DbException; /** * Stores a transport and returns true if the transport was not previously @@ -135,8 +140,11 @@ public interface DatabaseComponent { Collection<TransportUpdate> generateTransportUpdates(ContactId c, int maxLatency) throws DbException; - /** Returns all groups to which the user could subscribe. */ - Collection<Group> getAvailableGroups() throws DbException; + /** + * Returns all groups belonging to the given client to which the user could + * subscribe. + */ + Collection<Group> getAvailableGroups(ClientId c) throws DbException; /** Returns the contact with the given ID. */ Contact getContact(ContactId c) throws DbException; @@ -147,21 +155,11 @@ public interface DatabaseComponent { /** Returns the group with the given ID, if the user subscribes to it. */ Group getGroup(GroupId g) throws DbException; - /** Returns all groups to which the user subscribes, excluding inboxes. */ - Collection<Group> getGroups() throws DbException; - /** - * Returns the ID of the inbox group for the given contact, or null if no - * inbox group has been set. + * Returns all groups belonging to the given client to which the user + * subscribes. */ - GroupId getInboxGroupId(ContactId c) throws DbException; - - /** - * Returns the headers of all messages in the inbox group for the given - * contact, or null if no inbox group has been set. - */ - Collection<MessageHeader> getInboxMessageHeaders(ContactId c) - throws DbException; + Collection<Group> getGroups(ClientId c) throws DbException; /** Returns the local pseudonym with the given ID. */ LocalAuthor getLocalAuthor(AuthorId a) throws DbException; @@ -176,15 +174,35 @@ public interface DatabaseComponent { /** Returns the local transport properties for the given transport. */ TransportProperties getLocalProperties(TransportId t) throws DbException; - /** Returns the body of the message with the given ID. */ - byte[] getMessageBody(MessageId m) throws DbException; + /** + * Returns the IDs of any messages that need to be validated by the given + * client. + */ + Collection<MessageId> getMessagesToValidate(ClientId c) throws DbException; + + /** Returns the message with the given ID, in serialised form. */ + byte[] getRawMessage(MessageId m) throws DbException; + + /** Returns the metadata for all messages in the given group. */ + Map<MessageId, Metadata> getMessageMetadata(GroupId g) + throws DbException; + + /** Returns the metadata for the given message. */ + Metadata getMessageMetadata(MessageId m) throws DbException; - /** Returns the headers of all messages in the given group. */ - Collection<MessageHeader> getMessageHeaders(GroupId g) + /** + * Returns the status of all messages in the given group with respect to + * the given contact. + */ + Collection<MessageStatus> getMessageStatus(ContactId c, GroupId g) throws DbException; - /** Returns true if the given message is marked as read. */ - boolean getReadFlag(MessageId m) throws DbException; + /** + * Returns the status of the given message with respect to the given + * contact. + */ + MessageStatus getMessageStatus(ContactId c, MessageId m) + throws DbException; /** Returns all remote transport properties for the given transport. */ Map<ContactId, TransportProperties> getRemoteProperties(TransportId t) @@ -203,9 +221,6 @@ public interface DatabaseComponent { /** Returns the maximum latencies in milliseconds of all transports. */ Map<TransportId, Integer> getTransportLatencies() throws DbException; - /** Returns the number of unread messages in each subscribed group. */ - Map<GroupId, Integer> getUnreadMessageCounts() throws DbException; - /** Returns the IDs of all contacts to which the given group is visible. */ Collection<ContactId> getVisibility(GroupId g) throws DbException; @@ -223,6 +238,12 @@ public interface DatabaseComponent { void mergeLocalProperties(TransportId t, TransportProperties p) throws DbException; + /** + * Merges the given metadata with the existing metadata for the given + * message. + */ + void mergeMessageMetadata(MessageId m, Metadata meta) throws DbException; + /** * Merges the given settings with the existing settings in the given * namespace. @@ -276,16 +297,9 @@ public interface DatabaseComponent { */ void removeTransport(TransportId t) throws DbException; - /** - * Makes a group visible to the given contact, adds it to the contact's - * subscriptions, and sets it as the inbox group for the contact. - */ - void setInboxGroup(ContactId c, Group g) throws DbException; - - /** - * Marks a message as read or unread. - */ - void setReadFlag(MessageId m, boolean read) throws DbException; + /** Marks the given message as valid or invalid. */ + void setMessageValidity(Message m, ClientId c, boolean valid) + throws DbException; /** * Sets the remote transport properties for the given contact, replacing diff --git a/briar-api/src/org/briarproject/api/db/MessageExistsException.java b/briar-api/src/org/briarproject/api/db/MessageExistsException.java new file mode 100644 index 0000000000000000000000000000000000000000..7a3a6d2e00f5f42737bf9b78a3e72d553056feeb --- /dev/null +++ b/briar-api/src/org/briarproject/api/db/MessageExistsException.java @@ -0,0 +1,8 @@ +package org.briarproject.api.db; + +/** + * Thrown when a duplicate message is added to the database. This exception may + * occur due to concurrent updates and does not indicate a database error. + */ +public class MessageExistsException extends DbException { +} diff --git a/briar-api/src/org/briarproject/api/event/MessageAddedEvent.java b/briar-api/src/org/briarproject/api/event/MessageAddedEvent.java index 5e08c9b20b9f7930383ee829aeca306015405524..dca0212fdc53d2009b2f1e1f09ccb0b82cf4cd46 100644 --- a/briar-api/src/org/briarproject/api/event/MessageAddedEvent.java +++ b/briar-api/src/org/briarproject/api/event/MessageAddedEvent.java @@ -22,7 +22,7 @@ public class MessageAddedEvent extends Event { /** Returns the ID of the group to which the message belongs. */ public GroupId getGroupId() { - return message.getGroup().getId(); + return message.getGroupId(); } /** diff --git a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..c53a8363619ced08be3438d34026b9ba5aa1ef18 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java @@ -0,0 +1,38 @@ +package org.briarproject.api.event; + +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Message; + +/** + * An event that is broadcast when a message has passed or failed validation. + */ +public class MessageValidatedEvent extends Event { + + private final Message message; + private final ClientId clientId; + private final boolean local, valid; + + public MessageValidatedEvent(Message message, ClientId clientId, + boolean local, boolean valid) { + this.message = message; + this.clientId = clientId; + this.local = local; + this.valid = valid; + } + + public Message getMessage() { + return message; + } + + public ClientId getClientId() { + return clientId; + } + + public boolean isLocal() { + return local; + } + + public boolean isValid() { + return valid; + } +} diff --git a/briar-api/src/org/briarproject/api/forum/Forum.java b/briar-api/src/org/briarproject/api/forum/Forum.java index bad5f6ebf004767962a9dd6be700837bbd4fb236..720744a9695976c2f847be5c769ac3499899bbfd 100644 --- a/briar-api/src/org/briarproject/api/forum/Forum.java +++ b/briar-api/src/org/briarproject/api/forum/Forum.java @@ -1,10 +1,37 @@ package org.briarproject.api.forum; +import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; -public interface Forum { +public class Forum { - GroupId getId(); + private final Group group; + private final String name; - String getName(); + public Forum(Group group, String name) { + this.group = group; + this.name = name; + } + + public GroupId getId() { + return group.getId(); + } + + public Group getGroup() { + return group; + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + return group.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof Forum && group.equals(((Forum) o).group); + } } diff --git a/briar-api/src/org/briarproject/api/forum/ForumConstants.java b/briar-api/src/org/briarproject/api/forum/ForumConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..4c66fd14295a6081a0c192b331aa7da5861a05cb --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/ForumConstants.java @@ -0,0 +1,19 @@ +package org.briarproject.api.forum; + +import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; + +public interface ForumConstants { + + /** The maximum length of a forum's name in bytes. */ + int MAX_FORUM_NAME_LENGTH = MAX_GROUP_DESCRIPTOR_LENGTH - 10; + + /** The length of a forum's random salt in bytes. */ + int FORUM_SALT_LENGTH = 32; + + /** The maximum length of a forum post's content type in bytes. */ + int MAX_CONTENT_TYPE_LENGTH = 50; + + /** The maximum length of a forum post's body in bytes. */ + int MAX_FORUM_POST_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; +} diff --git a/briar-api/src/org/briarproject/api/forum/ForumFactory.java b/briar-api/src/org/briarproject/api/forum/ForumFactory.java deleted file mode 100644 index d069c63d7a554893f5429b26ef7c71ab0f10f215..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/forum/ForumFactory.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.briarproject.api.forum; - -public interface ForumFactory { - - /** Creates a forum with the given name and a random salt. */ - Forum createForum(String name); -} diff --git a/briar-api/src/org/briarproject/api/forum/ForumManager.java b/briar-api/src/org/briarproject/api/forum/ForumManager.java index 41df06559bb2f9c4f03adc9e8e30235cc61c27f5..f256eedd64ef56f923fe12fe71ac9574789a0abb 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumManager.java +++ b/briar-api/src/org/briarproject/api/forum/ForumManager.java @@ -3,14 +3,20 @@ package org.briarproject.api.forum; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DbException; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; -import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import java.util.Collection; public interface ForumManager { + /** Returns the unique ID of the forum client. */ + ClientId getClientId(); + + /** Creates a forum with the given name. */ + Forum createForum(String name); + /** * Subscribes to a forum, or returns false if the user already has the * maximum number of forum subscriptions. @@ -18,7 +24,7 @@ public interface ForumManager { boolean addForum(Forum f) throws DbException; /** Stores a local forum post. */ - void addLocalPost(Message m) throws DbException; + void addLocalPost(ForumPost p) throws DbException; /** Returns all forums to which the user could subscribe. */ Collection<Forum> getAvailableForums() throws DbException; diff --git a/briar-api/src/org/briarproject/api/forum/ForumPost.java b/briar-api/src/org/briarproject/api/forum/ForumPost.java new file mode 100644 index 0000000000000000000000000000000000000000..404d6c520e8cb33661298fa48a7acc123c278e31 --- /dev/null +++ b/briar-api/src/org/briarproject/api/forum/ForumPost.java @@ -0,0 +1,37 @@ +package org.briarproject.api.forum; + +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; + +public class ForumPost { + + private final Message message; + private final MessageId parent; + private final Author author; + private final String contentType; + + public ForumPost(Message message, MessageId parent, Author author, + String contentType) { + this.message = message; + this.parent = parent; + this.author = author; + this.contentType = contentType; + } + + public Message getMessage() { + return message; + } + + public MessageId getParent() { + return parent; + } + + public Author getAuthor() { + return author; + } + + public String getContentType() { + return contentType; + } +} diff --git a/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java b/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java index 185eb16b003880caf569d2043eb66aece0973822..3bcfd7b7d0db546d0358b6660e58cf80fdb80138 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java +++ b/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java @@ -2,7 +2,7 @@ package org.briarproject.api.forum; import org.briarproject.api.crypto.PrivateKey; import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import java.io.IOException; @@ -10,12 +10,12 @@ import java.security.GeneralSecurityException; public interface ForumPostFactory { - Message createAnonymousPost(MessageId parent, Forum forum, - String contentType, long timestamp, byte[] body) throws IOException, - GeneralSecurityException; + ForumPost createAnonymousPost(GroupId groupId, long timestamp, + MessageId parent, String contentType, byte[] body) + throws IOException, GeneralSecurityException; - Message createPseudonymousPost(MessageId parent, Forum forum, - Author author, PrivateKey privateKey, String contentType, - long timestamp, byte[] body) throws IOException, + ForumPost createPseudonymousPost(GroupId groupId, long timestamp, + MessageId parent, Author author, String contentType, byte[] body, + PrivateKey privateKey) throws IOException, GeneralSecurityException; } diff --git a/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java b/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java index ba861fca6f16c9c3619615dbac0f0e0ad588cd4e..15030223eeac2888443e2daee453be1bf8bc1150 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java +++ b/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java @@ -3,17 +3,46 @@ package org.briarproject.api.forum; import org.briarproject.api.identity.Author; import org.briarproject.api.sync.MessageId; -public interface ForumPostHeader { - - MessageId getId(); - - Author getAuthor(); - - Author.Status getAuthorStatus(); - - String getContentType(); - - long getTimestamp(); - - boolean isRead(); +public class ForumPostHeader { + + private final MessageId id; + private final long timestamp; + private final Author author; + private final Author.Status authorStatus; + private final String contentType; + private final boolean read; + + public ForumPostHeader(MessageId id, long timestamp, Author author, + Author.Status authorStatus, String contentType, boolean read) { + this.id = id; + this.timestamp = timestamp; + this.author = author; + this.authorStatus = authorStatus; + this.contentType = contentType; + this.read = read; + } + + public MessageId getId() { + return id; + } + + public Author getAuthor() { + return author; + } + + public Author.Status getAuthorStatus() { + return authorStatus; + } + + public String getContentType() { + return contentType; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isRead() { + return read; + } } diff --git a/briar-api/src/org/briarproject/api/identity/AuthorId.java b/briar-api/src/org/briarproject/api/identity/AuthorId.java index b0fd7d10dd496637a15c1553da97699879829192..4e346239c7e09325c70af864cf09773f76752107 100644 --- a/briar-api/src/org/briarproject/api/identity/AuthorId.java +++ b/briar-api/src/org/briarproject/api/identity/AuthorId.java @@ -2,22 +2,25 @@ package org.briarproject.api.identity; import org.briarproject.api.UniqueId; +import java.nio.charset.Charset; import java.util.Arrays; /** * Type-safe wrapper for a byte array that uniquely identifies an - * {@link Author}. + * {@link org.briarproject.api.identity.Author Author}. */ public class AuthorId extends UniqueId { + /** Label for hashing authors to calculate their identities. */ + public static final byte[] LABEL = + "AUTHOR_ID".getBytes(Charset.forName("US-ASCII")); + public AuthorId(byte[] id) { super(id); } @Override public boolean equals(Object o) { - if (o instanceof AuthorId) - return Arrays.equals(id, ((AuthorId) o).id); - return false; + return o instanceof AuthorId && Arrays.equals(id, ((AuthorId) o).id); } } diff --git a/briar-api/src/org/briarproject/api/messaging/MessagingConstants.java b/briar-api/src/org/briarproject/api/messaging/MessagingConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..98750b5518c58a0b487b62d2a8d416dd7903588c --- /dev/null +++ b/briar-api/src/org/briarproject/api/messaging/MessagingConstants.java @@ -0,0 +1,12 @@ +package org.briarproject.api.messaging; + +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; + +public interface MessagingConstants { + + /** The maximum length of a private message's content type in bytes. */ + int MAX_CONTENT_TYPE_LENGTH = 50; + + /** The maximum length of a private message's body in bytes. */ + int MAX_PRIVATE_MESSAGE_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; +} diff --git a/briar-api/src/org/briarproject/api/messaging/MessagingManager.java b/briar-api/src/org/briarproject/api/messaging/MessagingManager.java index ade6ab4c745d72044feb0f4592efd621d4c2b9ba..7ce3241f05c2c6bd379fa1487c035f5e7daad534 100644 --- a/briar-api/src/org/briarproject/api/messaging/MessagingManager.java +++ b/briar-api/src/org/briarproject/api/messaging/MessagingManager.java @@ -1,37 +1,35 @@ package org.briarproject.api.messaging; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.crypto.SecretKey; import org.briarproject.api.db.DbException; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; -import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import java.util.Collection; public interface MessagingManager { + /** Returns the unique ID of the messaging client. */ + ClientId getClientId(); + /** * Informs the messaging manager that a new contact has been added. * Creates a private conversation with the contact. */ - void addContact(ContactId c, SecretKey master) throws DbException; + void addContact(ContactId c) throws DbException; /** Stores a local private message. */ - void addLocalMessage(Message m) throws DbException; + void addLocalMessage(PrivateMessage m) throws DbException; - /** Returns the private conversation with the given ID. */ - PrivateConversation getConversation(GroupId g) throws DbException; + /** Returns the ID of the contact with the given private conversation. */ + ContactId getContactId(GroupId g) throws DbException; - /** - * Returns the ID of the private conversation with the given contact, or - * null if no private conversation ID has been set. - */ + /** Returns the ID of the private conversation with the given contact. */ GroupId getConversationId(ContactId c) throws DbException; /** - * Returns the headers of all messages in the private conversation with the - * given contact, or null if no private conversation ID has been set. + * Returns the headers of all messages in the given private conversation. */ Collection<PrivateMessageHeader> getMessageHeaders(ContactId c) throws DbException; @@ -39,13 +37,6 @@ public interface MessagingManager { /** Returns the body of the private message with the given ID. */ byte[] getMessageBody(MessageId m) throws DbException; - /** - * Makes a private conversation visible to the given contact, adds it to - * the contact's subscriptions, and sets it as the private conversation for - * the contact. - */ - void setConversation(ContactId c, PrivateConversation p) throws DbException; - /** Marks a private message as read or unread. */ void setReadFlag(MessageId m, boolean read) throws DbException; } diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateConversation.java b/briar-api/src/org/briarproject/api/messaging/PrivateConversation.java index a94d183550eda4f4b9f00a9e2b327aceda6ea653..0f03ed910e403872bc66603f4a22d617082f112f 100644 --- a/briar-api/src/org/briarproject/api/messaging/PrivateConversation.java +++ b/briar-api/src/org/briarproject/api/messaging/PrivateConversation.java @@ -1,8 +1,33 @@ package org.briarproject.api.messaging; +import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; -public interface PrivateConversation { +// TODO: Remove if no longer needed +public class PrivateConversation { - GroupId getId(); + private final Group group; + + public PrivateConversation(Group group) { + this.group = group; + } + + public GroupId getId() { + return group.getId(); + } + + public Group getGroup() { + return group; + } + + @Override + public int hashCode() { + return group.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof PrivateConversation + && group.equals(((PrivateConversation) o).group); + } } diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateMessage.java b/briar-api/src/org/briarproject/api/messaging/PrivateMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..7a51b374d81da43bec1da19063b51b69caddc8e1 --- /dev/null +++ b/briar-api/src/org/briarproject/api/messaging/PrivateMessage.java @@ -0,0 +1,30 @@ +package org.briarproject.api.messaging; + +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; + +public class PrivateMessage { + + private final Message message; + private final MessageId parent; + private final String contentType; + + public PrivateMessage(Message message, MessageId parent, + String contentType) { + this.message = message; + this.parent = parent; + this.contentType = contentType; + } + + public Message getMessage() { + return message; + } + + public MessageId getParent() { + return parent; + } + + public String getContentType() { + return contentType; + } +} diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java b/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java index 09c5102c800545cb1aa82526b2139fe60e8c63e3..72fded05b20215a0d16e617c2cb7cda26c1b9113 100644 --- a/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java +++ b/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java @@ -1,6 +1,6 @@ package org.briarproject.api.messaging; -import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import java.io.IOException; @@ -8,8 +8,7 @@ import java.security.GeneralSecurityException; public interface PrivateMessageFactory { - Message createPrivateMessage(MessageId parent, - PrivateConversation conversation, String contentType, - long timestamp, byte[] body) throws IOException, - GeneralSecurityException; + PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, + MessageId parent, String contentType, byte[] body) + throws IOException, GeneralSecurityException; } diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java index 1f81e21d13ef7cc40966d29293ef05614a364cd5..9db8854a1d819a04df07b8446fd30adaa8ea4ff4 100644 --- a/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java +++ b/briar-api/src/org/briarproject/api/messaging/PrivateMessageHeader.java @@ -1,23 +1,51 @@ package org.briarproject.api.messaging; -import org.briarproject.api.identity.Author; import org.briarproject.api.sync.MessageId; -public interface PrivateMessageHeader { - - enum Status { STORED, SENT, DELIVERED } - - MessageId getId(); - - Author getAuthor(); - - String getContentType(); - - long getTimestamp(); - - boolean isLocal(); - - boolean isRead(); - - Status getStatus(); +public class PrivateMessageHeader { + + private final MessageId id; + private final long timestamp; + private final String contentType; + private final boolean local, read, sent, seen; + + public PrivateMessageHeader(MessageId id, long timestamp, + String contentType, boolean local, boolean read, boolean sent, + boolean seen) { + this.id = id; + this.timestamp = timestamp; + this.contentType = contentType; + this.local = local; + this.read = read; + this.sent = sent; + this.seen = seen; + } + + public MessageId getId() { + return id; + } + + public String getContentType() { + return contentType; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isLocal() { + return local; + } + + public boolean isRead() { + return read; + } + + public boolean isSent() { + return sent; + } + + public boolean isSeen() { + return seen; + } } diff --git a/briar-api/src/org/briarproject/api/sync/ClientId.java b/briar-api/src/org/briarproject/api/sync/ClientId.java new file mode 100644 index 0000000000000000000000000000000000000000..d99f5c18dffa79668ac1a24c8dae48d1494b35c6 --- /dev/null +++ b/briar-api/src/org/briarproject/api/sync/ClientId.java @@ -0,0 +1,20 @@ +package org.briarproject.api.sync; + +import org.briarproject.api.UniqueId; + +import java.util.Arrays; + +/** + * Type-safe wrapper for a byte array that uniquely identifies a sync client. + */ +public class ClientId extends UniqueId { + + public ClientId(byte[] id) { + super(id); + } + + @Override + public boolean equals(Object o) { + return o instanceof ClientId && Arrays.equals(id, ((ClientId) o).id); + } +} diff --git a/briar-api/src/org/briarproject/api/sync/Group.java b/briar-api/src/org/briarproject/api/sync/Group.java index c10accdb8491fcc77ee186f2a991a76a07232478..51818a3822ba93f08455d7f62ada6e6b9f880b4e 100644 --- a/briar-api/src/org/briarproject/api/sync/Group.java +++ b/briar-api/src/org/briarproject/api/sync/Group.java @@ -1,28 +1,20 @@ package org.briarproject.api.sync; -import java.io.UnsupportedEncodingException; +import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; /** A group to which users may subscribe. */ public class Group { private final GroupId id; - private final String name; - private final byte[] salt; - - public Group(GroupId id, String name, byte[] salt) { - int length; - try { - length = name.getBytes("UTF-8").length; - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - if (length == 0 || length > MessagingConstants.MAX_GROUP_NAME_LENGTH) - throw new IllegalArgumentException(); - if (salt.length != MessagingConstants.GROUP_SALT_LENGTH) + private final ClientId clientId; + private final byte[] descriptor; + + public Group(GroupId id, ClientId clientId, byte[] descriptor) { + if (descriptor.length > MAX_GROUP_DESCRIPTOR_LENGTH) throw new IllegalArgumentException(); this.id = id; - this.name = name; - this.salt = salt; + this.clientId = clientId; + this.descriptor = descriptor; } /** Returns the group's unique identifier. */ @@ -30,17 +22,14 @@ public class Group { return id; } - /** Returns the group's name. */ - public String getName() { - return name; + /** Returns the ID of the client to which the group belongs. */ + public ClientId getClientId() { + return clientId; } - /** - * Returns the salt used to distinguish the group from other groups with - * the same name. - */ - public byte[] getSalt() { - return salt; + /** Returns the group's descriptor. */ + public byte[] getDescriptor() { + return descriptor; } @Override diff --git a/briar-api/src/org/briarproject/api/sync/GroupFactory.java b/briar-api/src/org/briarproject/api/sync/GroupFactory.java index 2850bec19cce6ec438ad3aa6b50c308b59be1272..02187cb8f58155356797569c4539bf57708ce099 100644 --- a/briar-api/src/org/briarproject/api/sync/GroupFactory.java +++ b/briar-api/src/org/briarproject/api/sync/GroupFactory.java @@ -2,9 +2,6 @@ package org.briarproject.api.sync; public interface GroupFactory { - /** Creates a group with the given name and a random salt. */ - Group createGroup(String name); - - /** Creates a group with the given name and salt. */ - Group createGroup(String name, byte[] salt); + /** Creates a group with the given client ID and descriptor. */ + Group createGroup(ClientId c, byte[] descriptor); } diff --git a/briar-api/src/org/briarproject/api/sync/GroupId.java b/briar-api/src/org/briarproject/api/sync/GroupId.java index dc5a4b9239cfee5c344196a04e8a335df9186bbf..6a86445578f0c80f740114b71ba30161a3766205 100644 --- a/briar-api/src/org/briarproject/api/sync/GroupId.java +++ b/briar-api/src/org/briarproject/api/sync/GroupId.java @@ -2,6 +2,7 @@ package org.briarproject.api.sync; import org.briarproject.api.UniqueId; +import java.nio.charset.Charset; import java.util.Arrays; /** @@ -9,14 +10,16 @@ import java.util.Arrays; */ public class GroupId extends UniqueId { + /** Label for hashing groups to calculate their identifiers. */ + public static final byte[] LABEL = + "GROUP_ID".getBytes(Charset.forName("US-ASCII")); + public GroupId(byte[] id) { super(id); } @Override public boolean equals(Object o) { - if (o instanceof GroupId) - return Arrays.equals(id, ((GroupId) o).id); - return false; + return o instanceof GroupId && Arrays.equals(id, ((GroupId) o).id); } } diff --git a/briar-api/src/org/briarproject/api/sync/Message.java b/briar-api/src/org/briarproject/api/sync/Message.java index 14c16d371dd9839e1d8fce457a8191f3e6209a3e..049807ac4b25ead9554860ce1229635bd6d23fc3 100644 --- a/briar-api/src/org/briarproject/api/sync/Message.java +++ b/briar-api/src/org/briarproject/api/sync/Message.java @@ -1,42 +1,58 @@ package org.briarproject.api.sync; -import org.briarproject.api.identity.Author; - -public interface Message { +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; + +public class Message { + + private final MessageId id; + private final GroupId groupId; + private final long timestamp; + private final byte[] raw; + + public Message(MessageId id, GroupId groupId, long timestamp, byte[] raw) { + if (raw.length <= MESSAGE_HEADER_LENGTH) + throw new IllegalArgumentException(); + if (raw.length > MAX_MESSAGE_LENGTH) + throw new IllegalArgumentException(); + this.id = id; + this.groupId = groupId; + this.timestamp = timestamp; + this.raw = raw; + } /** Returns the message's unique identifier. */ - MessageId getId(); - - /** - * Returns the identifier of the message's parent, or null if this is the - * first message in a thread. - */ - MessageId getParent(); - - /** - * Returns the {@link Group} to which the message belongs, or null if this - * is a private message. - */ - Group getGroup(); - - /** - * Returns the message's {@link Author Author}, or null - * if this is an anonymous message. - */ - Author getAuthor(); + public MessageId getId() { + return id; + } - /** Returns the message's content type. */ - String getContentType(); + /** Returns the ID of the {@link Group} to which the message belongs. */ + public GroupId getGroupId() { + return groupId; + } /** Returns the message's timestamp in milliseconds since the Unix epoch. */ - long getTimestamp(); - - /** Returns the serialised message. */ - byte[] getSerialised(); - - /** Returns the offset of the message body within the serialised message. */ - int getBodyStart(); - - /** Returns the length of the message body in bytes. */ - int getBodyLength(); + public long getTimestamp() { + return timestamp; + } + + /** Returns the length of the raw message in bytes. */ + public int getLength() { + return raw.length; + } + + /** Returns the raw message. */ + public byte[] getRaw() { + return raw; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof Message && id.equals(((Message) o).getId()); + } } \ No newline at end of file diff --git a/briar-api/src/org/briarproject/api/sync/MessageFactory.java b/briar-api/src/org/briarproject/api/sync/MessageFactory.java index b54070d78cd8eac8173d8f0dfeda5c056fd6c442..6e65fc44cd802225abe22d44a97e037d35410c10 100644 --- a/briar-api/src/org/briarproject/api/sync/MessageFactory.java +++ b/briar-api/src/org/briarproject/api/sync/MessageFactory.java @@ -1,19 +1,9 @@ package org.briarproject.api.sync; -import org.briarproject.api.crypto.PrivateKey; -import org.briarproject.api.identity.Author; - import java.io.IOException; -import java.security.GeneralSecurityException; public interface MessageFactory { - Message createAnonymousMessage(MessageId parent, Group group, - String contentType, long timestamp, byte[] body) throws IOException, - GeneralSecurityException; - - Message createPseudonymousMessage(MessageId parent, Group group, - Author author, PrivateKey privateKey, String contentType, - long timestamp, byte[] body) throws IOException, - GeneralSecurityException; + Message createMessage(GroupId groupId, long timestamp, byte[] body) + throws IOException; } diff --git a/briar-api/src/org/briarproject/api/sync/MessageHeader.java b/briar-api/src/org/briarproject/api/sync/MessageHeader.java deleted file mode 100644 index 701fbf5474154c1099b0b477adc1b6d3c9012990..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/sync/MessageHeader.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.briarproject.api.sync; - -import org.briarproject.api.identity.Author; - -public class MessageHeader { - - public enum State { STORED, SENT, DELIVERED } - - private final MessageId id, parent; - private final GroupId groupId; - private final Author author; - private final Author.Status authorStatus; - private final String contentType; - private final long timestamp; - private final boolean local, read; - private final State status; - - public MessageHeader(MessageId id, MessageId parent, GroupId groupId, - Author author, Author.Status authorStatus, String contentType, - long timestamp, boolean local, boolean read, State status) { - this.id = id; - this.parent = parent; - this.groupId = groupId; - this.author = author; - this.authorStatus = authorStatus; - this.contentType = contentType; - this.timestamp = timestamp; - this.local = local; - this.read = read; - this.status = status; - } - - /** Returns the message's unique identifier. */ - public MessageId getId() { - return id; - } - - /** - * Returns the message's parent, or null if this is the first message in a - * thread. - */ - public MessageId getParent() { - return parent; - } - - /** - * Returns the unique identifier of the group to which the message belongs. - */ - public GroupId getGroupId() { - return groupId; - } - - /** - * Returns the message's author, or null if this is an anonymous message. - */ - public Author getAuthor() { - return author; - } - - /** Returns the status of the message's author. */ - public Author.Status getAuthorStatus() { - return authorStatus; - } - - /** Returns the message's content type. */ - public String getContentType() { - return contentType; - } - - /** Returns the timestamp created by the message's author. */ - public long getTimestamp() { - return timestamp; - } - - /** Returns true if the message was locally generated. */ - public boolean isLocal() { - return local; - } - - /** Returns true if the message has been read. */ - public boolean isRead() { - return read; - } - - /** - * Returns message status. (This only applies to locally generated private - * messages.) - */ - public State getStatus() { - return status; - } -} diff --git a/briar-api/src/org/briarproject/api/sync/MessageId.java b/briar-api/src/org/briarproject/api/sync/MessageId.java index 472caec3fbe343c70402d75257592d8b191d809e..f437c511b4b09d56874e9a9ead439da00dda431d 100644 --- a/briar-api/src/org/briarproject/api/sync/MessageId.java +++ b/briar-api/src/org/briarproject/api/sync/MessageId.java @@ -2,6 +2,7 @@ package org.briarproject.api.sync; import org.briarproject.api.UniqueId; +import java.nio.charset.Charset; import java.util.Arrays; /** @@ -10,14 +11,16 @@ import java.util.Arrays; */ public class MessageId extends UniqueId { + /** Label for hashing messages to calculate their identifiers. */ + public static final byte[] LABEL = + "MESSAGE_ID".getBytes(Charset.forName("US-ASCII")); + public MessageId(byte[] id) { super(id); } @Override public boolean equals(Object o) { - if (o instanceof MessageId) - return Arrays.equals(id, ((MessageId) o).id); - return false; + return o instanceof MessageId && Arrays.equals(id, ((MessageId) o).id); } } diff --git a/briar-api/src/org/briarproject/api/sync/MessageStatus.java b/briar-api/src/org/briarproject/api/sync/MessageStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..ec1df8cc4879751e1445e27c3da0bd82d05815e7 --- /dev/null +++ b/briar-api/src/org/briarproject/api/sync/MessageStatus.java @@ -0,0 +1,38 @@ +package org.briarproject.api.sync; + +import org.briarproject.api.contact.ContactId; + +public class MessageStatus { + + private final MessageId messageId; + private final ContactId contactId; + private final boolean sent, seen; + + public MessageStatus(MessageId messageId, ContactId contactId, + boolean sent, boolean seen) { + this.messageId = messageId; + this.contactId = contactId; + this.sent = sent; + this.seen = seen; + } + + /** Returns the ID of the message. */ + public MessageId getMessageId() { + return messageId; + } + + /** Returns the ID of the contact. */ + public ContactId getContactId() { + return contactId; + } + + /** Returns true if the message has been sent to the contact. */ + public boolean isSent() { + return sent; + } + + /** Returns true if the message has been seen by the contact. */ + public boolean isSeen() { + return seen; + } +} diff --git a/briar-api/src/org/briarproject/api/sync/MessageValidator.java b/briar-api/src/org/briarproject/api/sync/MessageValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..d318421a472c16b5a78f597028c8786901477fd7 --- /dev/null +++ b/briar-api/src/org/briarproject/api/sync/MessageValidator.java @@ -0,0 +1,13 @@ +package org.briarproject.api.sync; + +import org.briarproject.api.db.Metadata; +import org.briarproject.api.lifecycle.Service; + +public interface MessageValidator extends Service { + + /** + * Validates the given message and returns its metadata if the message + * is valid, or null if the message is invalid. + */ + Metadata validateMessage(Message m); +} diff --git a/briar-api/src/org/briarproject/api/sync/MessageVerifier.java b/briar-api/src/org/briarproject/api/sync/MessageVerifier.java deleted file mode 100644 index 87ee02be696103372689cc05758aa0bcaeece0ed..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/sync/MessageVerifier.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.briarproject.api.sync; - -import java.security.GeneralSecurityException; - -/** Verifies the signatures on an {@link UnverifiedMessage}. */ -public interface MessageVerifier { - - Message verifyMessage(UnverifiedMessage m) throws GeneralSecurityException; -} diff --git a/briar-api/src/org/briarproject/api/sync/MessagingConstants.java b/briar-api/src/org/briarproject/api/sync/MessagingConstants.java deleted file mode 100644 index 5353f442f7365583b2b2ede9004a3cbaf92e2e47..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/sync/MessagingConstants.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.briarproject.api.sync; - - -public interface MessagingConstants { - - /** The current version of the messaging protocol. */ - byte PROTOCOL_VERSION = 0; - - /** The length of the packet header in bytes. */ - int HEADER_LENGTH = 4; - - /** The maximum length of the packet payload in bytes. */ - int MAX_PAYLOAD_LENGTH = 32 * 1024; // 32 KiB - - /** The maximum number of public groups a user may subscribe to. */ - int MAX_SUBSCRIPTIONS = 300; - - /** The maximum length of a group's name in UTF-8 bytes. */ - int MAX_GROUP_NAME_LENGTH = 50; - - /** The length of a group's random salt in bytes. */ - int GROUP_SALT_LENGTH = 32; - - /** - * The maximum length of a message body in bytes. To allow for future - * changes in the protocol, this is smaller than the maximum payload length - * even when all the message's other fields have their maximum lengths. - */ - int MAX_BODY_LENGTH = MAX_PAYLOAD_LENGTH - 1024; - - /** The maximum length of a message's content type in UTF-8 bytes. */ - int MAX_CONTENT_TYPE_LENGTH = 50; - - /** The maximum length of a message's subject line in UTF-8 bytes. */ - int MAX_SUBJECT_LENGTH = 100; - - /** The length of a message's random salt in bytes. */ - int MESSAGE_SALT_LENGTH = 32; - - /** - * When calculating the retention time of the database, the timestamp of - * the oldest message in the database is rounded down to a multiple of - * this value to avoid revealing the presence of any particular message. - */ - int RETENTION_GRANULARITY = 60 * 1000; // 1 minute -} diff --git a/briar-api/src/org/briarproject/api/sync/PacketReader.java b/briar-api/src/org/briarproject/api/sync/PacketReader.java index 8ee829e9f0b75913527cb549c225ffe11fdb126e..a942adc7cacd88c6c3c4cc66da03dce2e123d745 100644 --- a/briar-api/src/org/briarproject/api/sync/PacketReader.java +++ b/briar-api/src/org/briarproject/api/sync/PacketReader.java @@ -10,7 +10,7 @@ public interface PacketReader { Ack readAck() throws IOException; boolean hasMessage() throws IOException; - UnverifiedMessage readMessage() throws IOException; + Message readMessage() throws IOException; boolean hasOffer() throws IOException; Offer readOffer() throws IOException; diff --git a/briar-api/src/org/briarproject/api/sync/PacketTypes.java b/briar-api/src/org/briarproject/api/sync/PacketTypes.java index 873b89b3a81a93d90501b550e2834d0fbe025224..2bbeeae06b008bc38e7df96b8731826c05ae8897 100644 --- a/briar-api/src/org/briarproject/api/sync/PacketTypes.java +++ b/briar-api/src/org/briarproject/api/sync/PacketTypes.java @@ -1,6 +1,6 @@ package org.briarproject.api.sync; -/** Packet types for the messaging protocol. */ +/** Packet types for the sync protocol. */ public interface PacketTypes { byte ACK = 0; diff --git a/briar-api/src/org/briarproject/api/sync/SyncConstants.java b/briar-api/src/org/briarproject/api/sync/SyncConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..3cb297a4bd30e6ba51521f4cc1006768e06d06d4 --- /dev/null +++ b/briar-api/src/org/briarproject/api/sync/SyncConstants.java @@ -0,0 +1,30 @@ +package org.briarproject.api.sync; + +import org.briarproject.api.UniqueId; + +public interface SyncConstants { + + /** The current version of the sync protocol. */ + byte PROTOCOL_VERSION = 0; + + /** The length of the packet header in bytes. */ + int PACKET_HEADER_LENGTH = 4; + + /** The maximum length of the packet payload in bytes. */ + int MAX_PACKET_PAYLOAD_LENGTH = 32 * 1024; // 32 KiB + + /** The maximum number of groups a user may subscribe to. */ + int MAX_SUBSCRIPTIONS = 200; + + /** The maximum length of a group descriptor in bytes. */ + int MAX_GROUP_DESCRIPTOR_LENGTH = 100; + + /** The maximum length of a message in bytes. */ + int MAX_MESSAGE_LENGTH = MAX_PACKET_PAYLOAD_LENGTH - PACKET_HEADER_LENGTH; + + /** The length of the message header in bytes. */ + int MESSAGE_HEADER_LENGTH = UniqueId.LENGTH + 8; + + /** The maximum length of a message body in bytes. */ + int MAX_MESSAGE_BODY_LENGTH = MAX_MESSAGE_LENGTH - MESSAGE_HEADER_LENGTH; +} diff --git a/briar-api/src/org/briarproject/api/sync/MessagingSession.java b/briar-api/src/org/briarproject/api/sync/SyncSession.java similarity index 91% rename from briar-api/src/org/briarproject/api/sync/MessagingSession.java rename to briar-api/src/org/briarproject/api/sync/SyncSession.java index 17009b964497636d2c45a1e652acbe2f19886fdf..29378814170e4cd588ba308c4157fefa7a021744 100644 --- a/briar-api/src/org/briarproject/api/sync/MessagingSession.java +++ b/briar-api/src/org/briarproject/api/sync/SyncSession.java @@ -2,7 +2,7 @@ package org.briarproject.api.sync; import java.io.IOException; -public interface MessagingSession { +public interface SyncSession { /** * Runs the session. This method returns when there are no more packets to diff --git a/briar-api/src/org/briarproject/api/sync/MessagingSessionFactory.java b/briar-api/src/org/briarproject/api/sync/SyncSessionFactory.java similarity index 53% rename from briar-api/src/org/briarproject/api/sync/MessagingSessionFactory.java rename to briar-api/src/org/briarproject/api/sync/SyncSessionFactory.java index a54695dfe026e6c57b3d249dbcc597cf3530f229..b5db6a3493e9beba4af9ffcb9130325888de1c61 100644 --- a/briar-api/src/org/briarproject/api/sync/MessagingSessionFactory.java +++ b/briar-api/src/org/briarproject/api/sync/SyncSessionFactory.java @@ -6,14 +6,14 @@ import org.briarproject.api.contact.ContactId; import java.io.InputStream; import java.io.OutputStream; -public interface MessagingSessionFactory { +public interface SyncSessionFactory { - MessagingSession createIncomingSession(ContactId c, TransportId t, + SyncSession createIncomingSession(ContactId c, TransportId t, InputStream in); - MessagingSession createSimplexOutgoingSession(ContactId c, TransportId t, + SyncSession createSimplexOutgoingSession(ContactId c, TransportId t, int maxLatency, OutputStream out); - MessagingSession createDuplexOutgoingSession(ContactId c, TransportId t, + SyncSession createDuplexOutgoingSession(ContactId c, TransportId t, int maxLatency, int maxIdleTime, OutputStream out); } diff --git a/briar-api/src/org/briarproject/api/sync/UnverifiedMessage.java b/briar-api/src/org/briarproject/api/sync/UnverifiedMessage.java deleted file mode 100644 index 523cc1e305b1d3b145626b8248670aac61b557b5..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/sync/UnverifiedMessage.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.briarproject.api.sync; - -import org.briarproject.api.identity.Author; - -/** A {@link Message} that has not yet had its signatures (if any) verified. */ -public class UnverifiedMessage { - - private final MessageId parent; - private final Group group; - private final Author author; - private final String contentType; - private final long timestamp; - private final byte[] raw, signature; - private final int bodyStart, bodyLength, signedLength; - - public UnverifiedMessage(MessageId parent, Group group, Author author, - String contentType, long timestamp, byte[] raw, byte[] signature, - int bodyStart, int bodyLength, int signedLength) { - this.parent = parent; - this.group = group; - this.author = author; - this.contentType = contentType; - this.timestamp = timestamp; - this.raw = raw; - this.signature = signature; - this.bodyStart = bodyStart; - this.bodyLength = bodyLength; - this.signedLength = signedLength; - } - - /** - * Returns the identifier of the message's parent, or null if this is the - * first message in a thread. - */ - public MessageId getParent() { - return parent; - } - - /** - * Returns the {@link Group} to which the message belongs, or null if this - * is a private message. - */ - public Group getGroup() { - return group; - } - - /** - * Returns the message's {@link Author Author}, or null - * if this is an anonymous message. - */ - public Author getAuthor() { - return author; - } - - /** Returns the message's content type. */ - public String getContentType() { - return contentType; - } - - /** Returns the message's timestamp. */ - public long getTimestamp() { - return timestamp; - } - - /** Returns the serialised message. */ - public byte[] getSerialised() { - return raw; - } - - /** - * Returns the author's signature, or null if this is an anonymous message. - */ - public byte[] getSignature() { - return signature; - } - - /** Returns the offset of the message body within the serialised message. */ - public int getBodyStart() { - return bodyStart; - } - - /** Returns the length of the message body in bytes. */ - public int getBodyLength() { - return bodyLength; - } - - /** - * Returns the length in bytes of the data covered by the author's - * signature. - */ - public int getSignedLength() { - return signedLength; - } -} \ No newline at end of file diff --git a/briar-api/src/org/briarproject/api/sync/ValidationManager.java b/briar-api/src/org/briarproject/api/sync/ValidationManager.java new file mode 100644 index 0000000000000000000000000000000000000000..729e96ddccd6a47a06a100d4e1f2ac7af01c286b --- /dev/null +++ b/briar-api/src/org/briarproject/api/sync/ValidationManager.java @@ -0,0 +1,13 @@ +package org.briarproject.api.sync; + +import org.briarproject.api.lifecycle.Service; + +/** + * Responsible for managing message validators and passing them messages to + * validate. + */ +public interface ValidationManager extends Service { + + /** Sets the message validator for the given client. */ + void setMessageValidator(ClientId c, MessageValidator v); +} diff --git a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java index fc9b4bdb8a1df2434b37f4cb64a943705cfc5ba8..4decd40839d9bb1a95551ebc8bcafb90eae33120 100644 --- a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java +++ b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java @@ -73,8 +73,6 @@ class CryptoComponentImpl implements CryptoComponent { // KDF labels for signature nonce derivation private static final byte[] A_NONCE = ascii("ALICE_SIGNATURE_NONCE"); private static final byte[] B_NONCE = ascii("BOB_SIGNATURE_NONCE"); - // KDF label for group salt derivation - private static final byte[] SALT = ascii("SALT"); // KDF labels for tag key derivation private static final byte[] A_TAG = ascii("ALICE_TAG_KEY"); private static final byte[] B_TAG = ascii("BOB_TAG_KEY"); @@ -233,10 +231,6 @@ class CryptoComponentImpl implements CryptoComponent { return macKdf(master, alice ? A_NONCE : B_NONCE); } - public byte[] deriveGroupSalt(SecretKey master) { - return macKdf(master, SALT); - } - public TransportKeys deriveTransportKeys(TransportId t, SecretKey master, long rotationPeriod, boolean alice) { // Keys for the previous period are derived from the master secret @@ -325,6 +319,17 @@ class CryptoComponentImpl implements CryptoComponent { System.arraycopy(mac, 0, tag, 0, TAG_LENGTH); } + public byte[] hash(byte[]... inputs) { + MessageDigest digest = getMessageDigest(); + byte[] length = new byte[INT_32_BYTES]; + for (byte[] input : inputs) { + ByteUtils.writeUint32(input.length, length, 0); + digest.update(length); + digest.update(input); + } + return digest.digest(); + } + public byte[] encryptWithPassword(byte[] input, String password) { AuthenticatedCipher cipher = new XSalsa20Poly1305AuthenticatedCipher(); int macBytes = cipher.getMacBytes(); diff --git a/briar-core/src/org/briarproject/data/BdfReaderImpl.java b/briar-core/src/org/briarproject/data/BdfReaderImpl.java index 08ad3a3e342470c5d1550e4e66910d8ea9cbb0ed..71917b3bd912bb5fb3ef47578c2a265b32da132f 100644 --- a/briar-core/src/org/briarproject/data/BdfReaderImpl.java +++ b/briar-core/src/org/briarproject/data/BdfReaderImpl.java @@ -2,12 +2,9 @@ package org.briarproject.data; import org.briarproject.api.FormatException; import org.briarproject.api.data.BdfReader; -import org.briarproject.api.data.Consumer; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; import static org.briarproject.data.Types.DICTIONARY; import static org.briarproject.data.Types.END; @@ -33,7 +30,6 @@ class BdfReaderImpl implements BdfReader { private static final byte[] EMPTY_BUFFER = new byte[] {}; private final InputStream in; - private final Collection<Consumer> consumers = new ArrayList<Consumer>(0); private boolean hasLookahead = false, eof = false; private byte next; @@ -44,8 +40,8 @@ class BdfReaderImpl implements BdfReader { } private void readLookahead() throws IOException { - assert !eof; - assert !hasLookahead; + if (eof) throw new IllegalStateException(); + if (hasLookahead) throw new IllegalStateException(); // Read a lookahead byte int i = in.read(); if (i == -1) { @@ -56,27 +52,18 @@ class BdfReaderImpl implements BdfReader { hasLookahead = true; } - private void consumeLookahead() throws IOException { - assert hasLookahead; - for (Consumer c : consumers) c.write(next); - hasLookahead = false; - } - - private void readIntoBuffer(byte[] b, int length, boolean consume) - throws IOException { + private void readIntoBuffer(byte[] b, int length) throws IOException { int offset = 0; while (offset < length) { int read = in.read(b, offset, length - offset); if (read == -1) throw new FormatException(); offset += read; } - if (consume) for (Consumer c : consumers) c.write(b, 0, length); } - private void readIntoBuffer(int length, boolean consume) - throws IOException { + private void readIntoBuffer(int length) throws IOException { if (buf.length < length) buf = new byte[length]; - readIntoBuffer(buf, length, consume); + readIntoBuffer(buf, length); } private void skip(int length) throws IOException { @@ -108,14 +95,6 @@ class BdfReaderImpl implements BdfReader { in.close(); } - public void addConsumer(Consumer c) { - consumers.add(c); - } - - public void removeConsumer(Consumer c) { - if (!consumers.remove(c)) throw new IllegalArgumentException(); - } - public boolean hasNull() throws IOException { if (!hasLookahead) readLookahead(); if (eof) return false; @@ -124,7 +103,7 @@ class BdfReaderImpl implements BdfReader { public void readNull() throws IOException { if (!hasNull()) throw new FormatException(); - consumeLookahead(); + hasLookahead = false; } public void skipNull() throws IOException { @@ -141,7 +120,7 @@ class BdfReaderImpl implements BdfReader { public boolean readBoolean() throws IOException { if (!hasBoolean()) throw new FormatException(); boolean bool = next == TRUE; - consumeLookahead(); + hasLookahead = false; return bool; } @@ -159,32 +138,32 @@ class BdfReaderImpl implements BdfReader { public long readInteger() throws IOException { if (!hasInteger()) throw new FormatException(); - consumeLookahead(); - if (next == INT_8) return readInt8(true); - if (next == INT_16) return readInt16(true); - if (next == INT_32) return readInt32(true); - return readInt64(true); + hasLookahead = false; + if (next == INT_8) return readInt8(); + if (next == INT_16) return readInt16(); + if (next == INT_32) return readInt32(); + return readInt64(); } - private int readInt8(boolean consume) throws IOException { - readIntoBuffer(1, consume); + private int readInt8() throws IOException { + readIntoBuffer(1); return buf[0]; } - private short readInt16(boolean consume) throws IOException { - readIntoBuffer(2, consume); + private short readInt16() throws IOException { + readIntoBuffer(2); return (short) (((buf[0] & 0xFF) << 8) + (buf[1] & 0xFF)); } - private int readInt32(boolean consume) throws IOException { - readIntoBuffer(4, consume); + private int readInt32() throws IOException { + readIntoBuffer(4); int value = 0; for (int i = 0; i < 4; i++) value |= (buf[i] & 0xFF) << (24 - i * 8); return value; } - private long readInt64(boolean consume) throws IOException { - readIntoBuffer(8, consume); + private long readInt64() throws IOException { + readIntoBuffer(8); long value = 0; for (int i = 0; i < 8; i++) value |= (buf[i] & 0xFFL) << (56 - i * 8); return value; @@ -207,8 +186,8 @@ class BdfReaderImpl implements BdfReader { public double readFloat() throws IOException { if (!hasFloat()) throw new FormatException(); - consumeLookahead(); - readIntoBuffer(8, true); + hasLookahead = false; + readIntoBuffer(8); long value = 0; for (int i = 0; i < 8; i++) value |= (buf[i] & 0xFFL) << (56 - i * 8); return Double.longBitsToDouble(value); @@ -228,24 +207,24 @@ class BdfReaderImpl implements BdfReader { public String readString(int maxLength) throws IOException { if (!hasString()) throw new FormatException(); - consumeLookahead(); - int length = readStringLength(true); + hasLookahead = false; + int length = readStringLength(); if (length < 0 || length > maxLength) throw new FormatException(); if (length == 0) return ""; - readIntoBuffer(length, true); + readIntoBuffer(length); return new String(buf, 0, length, "UTF-8"); } - private int readStringLength(boolean consume) throws IOException { - if (next == STRING_8) return readInt8(consume); - if (next == STRING_16) return readInt16(consume); - if (next == STRING_32) return readInt32(consume); + private int readStringLength() throws IOException { + if (next == STRING_8) return readInt8(); + if (next == STRING_16) return readInt16(); + if (next == STRING_32) return readInt32(); throw new FormatException(); } public void skipString() throws IOException { if (!hasString()) throw new FormatException(); - int length = readStringLength(false); + int length = readStringLength(); if (length < 0) throw new FormatException(); skip(length); hasLookahead = false; @@ -259,25 +238,25 @@ class BdfReaderImpl implements BdfReader { public byte[] readRaw(int maxLength) throws IOException { if (!hasRaw()) throw new FormatException(); - consumeLookahead(); - int length = readRawLength(true); + hasLookahead = false; + int length = readRawLength(); if (length < 0 || length > maxLength) throw new FormatException(); if (length == 0) return EMPTY_BUFFER; byte[] b = new byte[length]; - readIntoBuffer(b, length, true); + readIntoBuffer(b, length); return b; } - private int readRawLength(boolean consume) throws IOException { - if (next == RAW_8) return readInt8(consume); - if (next == RAW_16) return readInt16(consume); - if (next == RAW_32) return readInt32(consume); + private int readRawLength() throws IOException { + if (next == RAW_8) return readInt8(); + if (next == RAW_16) return readInt16(); + if (next == RAW_32) return readInt32(); throw new FormatException(); } public void skipRaw() throws IOException { if (!hasRaw()) throw new FormatException(); - int length = readRawLength(false); + int length = readRawLength(); if (length < 0) throw new FormatException(); skip(length); hasLookahead = false; @@ -291,7 +270,7 @@ class BdfReaderImpl implements BdfReader { public void readListStart() throws IOException { if (!hasList()) throw new FormatException(); - consumeLookahead(); + hasLookahead = false; } public boolean hasListEnd() throws IOException { @@ -310,7 +289,7 @@ class BdfReaderImpl implements BdfReader { private void readEnd() throws IOException { if (!hasEnd()) throw new FormatException(); - consumeLookahead(); + hasLookahead = false; } public void skipList() throws IOException { @@ -328,7 +307,7 @@ class BdfReaderImpl implements BdfReader { public void readDictionaryStart() throws IOException { if (!hasDictionary()) throw new FormatException(); - consumeLookahead(); + hasLookahead = false; } public boolean hasDictionaryEnd() throws IOException { diff --git a/briar-core/src/org/briarproject/data/MetadataEncoderImpl.java b/briar-core/src/org/briarproject/data/MetadataEncoderImpl.java index f476df113476a9bf234c43a3891b9a28a5771d62..e70070391bdbcad505ac24eaf7b640e55890fca9 100644 --- a/briar-core/src/org/briarproject/data/MetadataEncoderImpl.java +++ b/briar-core/src/org/briarproject/data/MetadataEncoderImpl.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import static org.briarproject.api.data.BdfDictionary.NULL_VALUE; import static org.briarproject.api.db.Metadata.REMOVE; import static org.briarproject.data.Types.DICTIONARY; import static org.briarproject.data.Types.END; @@ -37,7 +38,7 @@ class MetadataEncoderImpl implements MetadataEncoder { Metadata m = new Metadata(); ByteArrayOutputStream out = new ByteArrayOutputStream(); for (Entry<String, Object> e : d.entrySet()) { - if (e.getValue() == null) { + if (e.getValue() == NULL_VALUE) { // Special case: if the value is null, the key is being removed m.put(e.getKey(), REMOVE); } else { @@ -51,7 +52,7 @@ class MetadataEncoderImpl implements MetadataEncoder { private void encodeObject(ByteArrayOutputStream out, Object o) throws FormatException { - if (o == null) out.write(NULL); + if (o == NULL_VALUE) out.write(NULL); else if (o instanceof Boolean) out.write((Boolean) o ? TRUE : FALSE); else if (o instanceof Byte) encodeInteger(out, (Byte) o); else if (o instanceof Short) encodeInteger(out, (Short) o); diff --git a/briar-core/src/org/briarproject/data/MetadataParserImpl.java b/briar-core/src/org/briarproject/data/MetadataParserImpl.java index ce4c71a3a1f0b09b7872afb5f6a92be76f50f986..eadaa34771ac34d624ec9a137cbdfbe31b7a8b9c 100644 --- a/briar-core/src/org/briarproject/data/MetadataParserImpl.java +++ b/briar-core/src/org/briarproject/data/MetadataParserImpl.java @@ -10,6 +10,7 @@ import org.briarproject.util.StringUtils; import java.io.ByteArrayInputStream; import java.util.Map.Entry; +import static org.briarproject.api.data.BdfDictionary.NULL_VALUE; import static org.briarproject.api.db.Metadata.REMOVE; import static org.briarproject.data.Types.DICTIONARY; import static org.briarproject.data.Types.END; @@ -33,14 +34,14 @@ class MetadataParserImpl implements MetadataParser { @Override public BdfDictionary parse(Metadata m) throws FormatException { - BdfDictionary dict = new BdfDictionary(); + BdfDictionary d = new BdfDictionary(); for (Entry<String, byte[]> e : m.entrySet()) - dict.put(e.getKey(), parseObject(e.getValue())); - return dict; + d.put(e.getKey(), parseValue(e.getValue())); + return d; } - private Object parseObject(byte[] b) throws FormatException { - if (b == REMOVE) return null; + private Object parseValue(byte[] b) throws FormatException { + if (b == REMOVE) return NULL_VALUE; ByteArrayInputStream in = new ByteArrayInputStream(b); Object o = parseObject(in); if (in.available() > 0) throw new FormatException(); @@ -50,7 +51,7 @@ class MetadataParserImpl implements MetadataParser { private Object parseObject(ByteArrayInputStream in) throws FormatException { switch(in.read()) { case NULL: - return null; + return NULL_VALUE; case TRUE: return Boolean.TRUE; case FALSE: diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java index 9963267fee251901ff844bad912a4637bec7bd9c..f512b855303e249fc994924cd5baa80737694241 100644 --- a/briar-core/src/org/briarproject/db/Database.java +++ b/briar-core/src/org/briarproject/db/Database.java @@ -6,14 +6,16 @@ import org.briarproject.api.TransportProperties; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageHeader; import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.TransportAck; @@ -85,6 +87,13 @@ interface Database<T> { ContactId addContact(T txn, Author remote, AuthorId local) throws DbException; + /** + * Adds a group to the given contact's subscriptions. + * <p> + * Locking: write. + */ + void addContactGroup(T txn, ContactId c, Group g) throws DbException; + /** * Subscribes to a group, or returns false if the user already has the * maximum number of subscriptions. @@ -216,11 +225,12 @@ interface Database<T> { int countOfferedMessages(T txn, ContactId c) throws DbException; /** - * Returns all groups to which the user could subscribe. + * Returns all groups belonging to the given client to which the user could + * subscribe. * <p> * Locking: read. */ - Collection<Group> getAvailableGroups(T txn) throws DbException; + Collection<Group> getAvailableGroups(T txn, ClientId c) throws DbException; /** * Returns the contact with the given ID. @@ -265,28 +275,12 @@ interface Database<T> { Group getGroup(T txn, GroupId g) throws DbException; /** - * Returns all groups to which the user subscribes, excluding inboxes. - * <p> - * Locking: read. - */ - Collection<Group> getGroups(T txn) throws DbException; - - /** - * Returns the ID of the inbox group for the given contact, or null if no - * inbox group has been set. + * Returns all groups belonging to the given client to which the user + * subscribes. * <p> * Locking: read. */ - GroupId getInboxGroupId(T txn, ContactId c) throws DbException; - - /** - * Returns the headers of all messages in the inbox group for the given - * contact, or null if no inbox group has been set. - * <p> - * Locking: read. - */ - Collection<MessageHeader> getInboxMessageHeaders(T txn, ContactId c) - throws DbException; + Collection<Group> getGroups(T txn, ClientId c) throws DbException; /** * Returns the local pseudonym with the given ID. @@ -319,19 +313,37 @@ interface Database<T> { throws DbException; /** - * Returns the body of the message identified by the given ID. + * Returns the metadata for all messages in the given group. * <p> * Locking: read. */ - byte[] getMessageBody(T txn, MessageId m) throws DbException; + Map<MessageId, Metadata> getMessageMetadata(T txn, GroupId g) + throws DbException; /** - * Returns the headers of all messages in the given group. + * Returns the metadata for the given message. * <p> * Locking: read. */ - Collection<MessageHeader> getMessageHeaders(T txn, GroupId g) - throws DbException; + Metadata getMessageMetadata(T txn, MessageId m) throws DbException; + + /** + * Returns the status of all messages in the given group with respect + * to the given contact. + * <p> + * Locking: read + */ + Collection<MessageStatus> getMessageStatus(T txn, ContactId c, GroupId g) + throws DbException; + + /** + * Returns the status of the given message with respect to the given + * contact. + * <p> + * Locking: read + */ + MessageStatus getMessageStatus(T txn, ContactId c, MessageId m) + throws DbException; /** * Returns the IDs of some messages received from the given contact that @@ -370,28 +382,21 @@ interface Database<T> { int maxMessages) throws DbException; /** - * Returns the parent of the given message, or null if either the message - * has no parent, or the parent is absent from the database, or the parent - * belongs to a different group. + * Returns the IDs of any messages that need to be validated by the given + * client. * <p> * Locking: read. */ - MessageId getParent(T txn, MessageId m) throws DbException; + Collection<MessageId> getMessagesToValidate(T txn, ClientId c) + throws DbException; /** - * Returns the message identified by the given ID, in serialised form. + * Returns the message with the given ID, in serialised form. * <p> * Locking: read. */ byte[] getRawMessage(T txn, MessageId m) throws DbException; - /** - * Returns true if the given message is marked as read. - * <p> - * Locking: read. - */ - boolean getReadFlag(T txn, MessageId m) throws DbException; - /** * Returns all remote properties for the given transport. * <p> @@ -475,13 +480,6 @@ interface Database<T> { Collection<TransportUpdate> getTransportUpdates(T txn, ContactId c, int maxLatency) throws DbException; - /** - * Returns the number of unread messages in each subscribed group. - * <p> - * Locking: read. - */ - Map<GroupId, Integer> getUnreadMessageCounts(T txn) throws DbException; - /** * Returns the IDs of all contacts to which the given group is visible. * <p> @@ -525,6 +523,15 @@ interface Database<T> { void mergeLocalProperties(T txn, TransportId t, TransportProperties p) throws DbException; + /* + * Merges the given metadata with the existing metadata for the given + * message. + * <p> + * Locking: write. + */ + void mergeMessageMetadata(T txn, MessageId m, Metadata meta) + throws DbException; + /** * Merges the given settings with the existing settings in the given * namespace. @@ -624,6 +631,10 @@ interface Database<T> { */ void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException; + /** Marks the given message as valid or invalid. */ + void setMessageValidity(T txn, MessageId m, boolean valid) + throws DbException; + /** * Sets the reordering window for the given contact and transport in the * given rotation period. @@ -634,30 +645,15 @@ interface Database<T> { long rotationPeriod, long base, byte[] bitmap) throws DbException; /** - * Updates the groups to which the given contact subscribes and returns - * true, unless an update with an equal or higher version number has - * already been received from the contact. + * Updates the given contact's subscriptions and returns true, unless an + * update with an equal or higher version number has already been received + * from the contact. * <p> * Locking: write. */ boolean setGroups(T txn, ContactId c, Collection<Group> groups, long version) throws DbException; - /** - * Makes a group visible to the given contact, adds it to the contact's - * subscriptions, and sets it as the inbox group for the contact. - * <p> - * Locking: write. - */ - void setInboxGroup(T txn, ContactId c, Group g) throws DbException; - - /** - * Marks a message as read or unread. - * <p> - * Locking: write. - */ - void setReadFlag(T txn, MessageId m, boolean read) throws DbException; - /** * Sets the remote transport properties for the given contact, replacing * any existing properties. diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 64e6e831668c6209b8e37f1dbd0649ba539faa59..3378b8ff537e468bc58a476ddf6bf3cf922daf48 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -9,6 +9,8 @@ import org.briarproject.api.db.ContactExistsException; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DbException; import org.briarproject.api.db.LocalAuthorExistsException; +import org.briarproject.api.db.MessageExistsException; +import org.briarproject.api.db.Metadata; import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.db.NoSuchLocalAuthorException; import org.briarproject.api.db.NoSuchMessageException; @@ -25,6 +27,7 @@ import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; @@ -39,11 +42,12 @@ import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.lifecycle.ShutdownManager; import org.briarproject.api.sync.Ack; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageHeader; import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.Request; import org.briarproject.api.sync.SubscriptionAck; @@ -165,6 +169,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { return c; } + public void addContactGroup(ContactId c, Group g) throws DbException { + lock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + db.addContactGroup(txn, c, g); + db.commitTransaction(txn); + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.writeLock().unlock(); + } + } + public boolean addGroup(Group g) throws DbException { boolean added = false; lock.writeLock().lock(); @@ -204,15 +224,18 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { eventBus.broadcast(new LocalAuthorAddedEvent(a.getId())); } - public void addLocalMessage(Message m) throws DbException { - boolean duplicate, subscribed; + public void addLocalMessage(Message m, ClientId c, Metadata meta) + throws DbException { lock.writeLock().lock(); try { T txn = db.startTransaction(); try { - duplicate = db.containsMessage(txn, m.getId()); - subscribed = db.containsGroup(txn, m.getGroup().getId()); - if (!duplicate && subscribed) addMessage(txn, m, null); + if (db.containsMessage(txn, m.getId())) + throw new MessageExistsException(); + if (!db.containsGroup(txn, m.getGroupId())) + throw new NoSuchSubscriptionException(); + addMessage(txn, m, null); + db.mergeMessageMetadata(txn, m.getId(), meta); db.commitTransaction(txn); } catch (DbException e) { db.abortTransaction(txn); @@ -221,28 +244,21 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } finally { lock.writeLock().unlock(); } - if (!duplicate && subscribed) { - eventBus.broadcast(new MessageAddedEvent(m, null)); - } + eventBus.broadcast(new MessageAddedEvent(m, null)); + eventBus.broadcast(new MessageValidatedEvent(m, c, true, true)); } /** - * Stores a message, initialises its status with respect to each contact, - * and marks it as read if it was locally generated. + * Stores a message and initialises its status with respect to each contact. * <p> * Locking: write. * @param sender null for a locally generated message. */ private void addMessage(T txn, Message m, ContactId sender) throws DbException { - if (sender == null) { - db.addMessage(txn, m, true); - db.setReadFlag(txn, m.getId(), true); - } else { - db.addMessage(txn, m, false); - } - Group g = m.getGroup(); - Collection<ContactId> visibility = db.getVisibility(txn, g.getId()); + db.addMessage(txn, m, sender == null); + GroupId g = m.getGroupId(); + Collection<ContactId> visibility = db.getVisibility(txn, g); visibility = new HashSet<ContactId>(visibility); for (ContactId c : db.getContactIds(txn)) { if (visibility.contains(c)) { @@ -506,12 +522,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public Collection<Group> getAvailableGroups() throws DbException { + public Collection<Group> getAvailableGroups(ClientId c) throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - Collection<Group> groups = db.getAvailableGroups(txn); + Collection<Group> groups = db.getAvailableGroups(txn, c); db.commitTransaction(txn); return groups; } catch (DbException e) { @@ -578,12 +594,12 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public Collection<Group> getGroups() throws DbException { + public Collection<Group> getGroups(ClientId c) throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - Collection<Group> groups = db.getGroups(txn); + Collection<Group> groups = db.getGroups(txn, c); db.commitTransaction(txn); return groups; } catch (DbException e) { @@ -595,16 +611,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public GroupId getInboxGroupId(ContactId c) throws DbException { + public LocalAuthor getLocalAuthor(AuthorId a) throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - if (!db.containsContact(txn, c)) - throw new NoSuchContactException(); - GroupId inbox = db.getInboxGroupId(txn, c); + if (!db.containsLocalAuthor(txn, a)) + throw new NoSuchLocalAuthorException(); + LocalAuthor localAuthor = db.getLocalAuthor(txn, a); db.commitTransaction(txn); - return inbox; + return localAuthor; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -614,18 +630,14 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public Collection<MessageHeader> getInboxMessageHeaders(ContactId c) - throws DbException { + public Collection<LocalAuthor> getLocalAuthors() throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - if (!db.containsContact(txn, c)) - throw new NoSuchContactException(); - Collection<MessageHeader> headers = - db.getInboxMessageHeaders(txn, c); + Collection<LocalAuthor> authors = db.getLocalAuthors(txn); db.commitTransaction(txn); - return headers; + return authors; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -635,16 +647,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public LocalAuthor getLocalAuthor(AuthorId a) throws DbException { + public Map<TransportId, TransportProperties> getLocalProperties() + throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - if (!db.containsLocalAuthor(txn, a)) - throw new NoSuchLocalAuthorException(); - LocalAuthor localAuthor = db.getLocalAuthor(txn, a); + Map<TransportId, TransportProperties> properties = + db.getLocalProperties(txn); db.commitTransaction(txn); - return localAuthor; + return properties; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -654,14 +666,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public Collection<LocalAuthor> getLocalAuthors() throws DbException { + public TransportProperties getLocalProperties(TransportId t) + throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - Collection<LocalAuthor> authors = db.getLocalAuthors(txn); + if (!db.containsTransport(txn, t)) + throw new NoSuchTransportException(); + TransportProperties properties = db.getLocalProperties(txn, t); db.commitTransaction(txn); - return authors; + return properties; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -671,16 +686,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public Map<TransportId, TransportProperties> getLocalProperties() + public Collection<MessageId> getMessagesToValidate(ClientId c) throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - Map<TransportId, TransportProperties> properties = - db.getLocalProperties(txn); + Collection<MessageId> ids = db.getMessagesToValidate(txn, c); db.commitTransaction(txn); - return properties; + return ids; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -690,17 +704,37 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public TransportProperties getLocalProperties(TransportId t) + public byte[] getRawMessage(MessageId m) throws DbException { + lock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + byte[] raw = db.getRawMessage(txn, m); + db.commitTransaction(txn); + return raw; + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.readLock().unlock(); + } + } + + public Map<MessageId, Metadata> getMessageMetadata(GroupId g) throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { - if (!db.containsTransport(txn, t)) - throw new NoSuchTransportException(); - TransportProperties properties = db.getLocalProperties(txn, t); + if (!db.containsGroup(txn, g)) + throw new NoSuchSubscriptionException(); + Map<MessageId, Metadata> metadata = + db.getMessageMetadata(txn, g); db.commitTransaction(txn); - return properties; + return metadata; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -710,16 +744,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public byte[] getMessageBody(MessageId m) throws DbException { + public Metadata getMessageMetadata(MessageId m) throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { if (!db.containsMessage(txn, m)) throw new NoSuchMessageException(); - byte[] body = db.getMessageBody(txn, m); + Metadata metadata = db.getMessageMetadata(txn, m); db.commitTransaction(txn); - return body; + return metadata; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -729,18 +763,20 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public Collection<MessageHeader> getMessageHeaders(GroupId g) + public Collection<MessageStatus> getMessageStatus(ContactId c, GroupId g) throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); if (!db.containsGroup(txn, g)) throw new NoSuchSubscriptionException(); - Collection<MessageHeader> headers = - db.getMessageHeaders(txn, g); + Collection<MessageStatus> statuses = + db.getMessageStatus(txn, c, g); db.commitTransaction(txn); - return headers; + return statuses; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -750,16 +786,19 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public boolean getReadFlag(MessageId m) throws DbException { + public MessageStatus getMessageStatus(ContactId c, MessageId m) + throws DbException { lock.readLock().lock(); try { T txn = db.startTransaction(); try { + if (!db.containsContact(txn, c)) + throw new NoSuchContactException(); if (!db.containsMessage(txn, m)) throw new NoSuchMessageException(); - boolean read = db.getReadFlag(txn, m); + MessageStatus status = db.getMessageStatus(txn, c, m); db.commitTransaction(txn); - return read; + return status; } catch (DbException e) { db.abortTransaction(txn); throw e; @@ -862,23 +901,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } } - public Map<GroupId, Integer> getUnreadMessageCounts() throws DbException { - lock.readLock().lock(); - try { - T txn = db.startTransaction(); - try { - Map<GroupId, Integer> counts = db.getUnreadMessageCounts(txn); - db.commitTransaction(txn); - return counts; - } catch (DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - lock.readLock().unlock(); - } - } - public Collection<ContactId> getVisibility(GroupId g) throws DbException { lock.readLock().lock(); try { @@ -943,6 +965,25 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (changed) eventBus.broadcast(new LocalTransportsUpdatedEvent()); } + public void mergeMessageMetadata(MessageId m, Metadata meta) + throws DbException { + lock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + if (!db.containsMessage(txn, m)) + throw new NoSuchMessageException(); + db.mergeMessageMetadata(txn, m, meta); + db.commitTransaction(txn); + } catch (DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + lock.writeLock().unlock(); + } + } + public void mergeSettings(Settings s, String namespace) throws DbException { boolean changed = false; lock.writeLock().lock(); @@ -998,7 +1039,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (!db.containsContact(txn, c)) throw new NoSuchContactException(); duplicate = db.containsMessage(txn, m.getId()); - visible = db.containsVisibleGroup(txn, c, m.getGroup().getId()); + visible = db.containsVisibleGroup(txn, c, m.getGroupId()); if (visible) { if (!duplicate) addMessage(txn, m, c); db.raiseAckFlag(txn, c, m.getId()); @@ -1012,9 +1053,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { lock.writeLock().unlock(); } if (visible) { - if (!duplicate) { + if (!duplicate) eventBus.broadcast(new MessageAddedEvent(m, c)); - } eventBus.broadcast(new MessageToAckEvent(c)); } } @@ -1170,8 +1210,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { try { if (!db.containsContact(txn, c)) throw new NoSuchContactException(); - GroupId g = db.getInboxGroupId(txn, c); - if (g != null) db.removeGroup(txn, g); db.removeContact(txn, c); db.commitTransaction(txn); } catch (DbException e) { @@ -1216,10 +1254,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { if (!db.containsLocalAuthor(txn, a)) throw new NoSuchLocalAuthorException(); affected = db.getContacts(txn, a); - for (ContactId c : affected) { - GroupId g = db.getInboxGroupId(txn, c); - if (g != null) db.removeGroup(txn, g); - } db.removeLocalAuthor(txn, a); db.commitTransaction(txn); } catch (DbException e) { @@ -1253,32 +1287,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { eventBus.broadcast(new TransportRemovedEvent(t)); } - public void setInboxGroup(ContactId c, Group g) throws DbException { - lock.writeLock().lock(); - try { - T txn = db.startTransaction(); - try { - if (!db.containsContact(txn, c)) - throw new NoSuchContactException(); - db.setInboxGroup(txn, c, g); - db.commitTransaction(txn); - } catch (DbException e) { - db.abortTransaction(txn); - throw e; - } - } finally { - lock.writeLock().unlock(); - } - } - - public void setReadFlag(MessageId m, boolean read) throws DbException { + public void setMessageValidity(Message m, ClientId c, boolean valid) + throws DbException { lock.writeLock().lock(); try { T txn = db.startTransaction(); try { - if (!db.containsMessage(txn, m)) + if (!db.containsMessage(txn, m.getId())) throw new NoSuchMessageException(); - db.setReadFlag(txn, m, read); + db.setMessageValidity(txn, m.getId(), valid); db.commitTransaction(txn); } catch (DbException e) { db.abortTransaction(txn); @@ -1287,6 +1304,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent { } finally { lock.writeLock().unlock(); } + eventBus.broadcast(new MessageValidatedEvent(m, c, false, valid)); } public void setRemoteProperties(ContactId c, diff --git a/briar-core/src/org/briarproject/db/DatabaseConstants.java b/briar-core/src/org/briarproject/db/DatabaseConstants.java index 25f325ff4d748b7be538cea3ea5a2fd58d26715a..2c06feca19f68c5806fdc9f95c5214634fb9af94 100644 --- a/briar-core/src/org/briarproject/db/DatabaseConstants.java +++ b/briar-core/src/org/briarproject/db/DatabaseConstants.java @@ -8,34 +8,4 @@ interface DatabaseConstants { * limit is reached, additional offers will not be stored. */ int MAX_OFFERED_MESSAGES = 1000; - - // FIXME: These should be configurable - - /** - * The minimum amount of space in bytes that should be kept free on the - * device where the database is stored. Whenever less than this much space - * is free, old messages will be expired from the database. - */ - long MIN_FREE_SPACE = 50 * 1024 * 1024; // 50 MiB - - /** - * The minimum amount of space in bytes that must be kept free on the device - * where the database is stored. If less than this much space is free and - * there are no more messages to expire, an Error will be thrown. - */ - long CRITICAL_FREE_SPACE = 10 * 1024 * 1024; // 10 MiB - - /** - * The amount of free space will be checked whenever this many transactions - * have been started since the last check. - * <p> - * FIXME: Increase this after implementing BTPv2 (smaller packets)? - */ - int MAX_TRANSACTIONS_BETWEEN_SPACE_CHECKS = 10; - - /** - * Up to this many bytes of messages will be expired from the database each - * time it is necessary to expire messages. - */ - int BYTES_PER_SWEEP = 10 * 1024 * 1024; // 10 MiB } diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java index b4f5d7932bc5cde9235a1e36690f72e2ca788fc7..80c9a1dd90d26ca7b86bfcf720c792bce3851431 100644 --- a/briar-core/src/org/briarproject/db/JdbcDatabase.java +++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java @@ -8,15 +8,16 @@ import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.SecretKey; import org.briarproject.api.db.DbClosedException; import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageHeader; -import org.briarproject.api.sync.MessageHeader.State; import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.TransportAck; @@ -48,13 +49,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; -import static java.sql.Types.BINARY; -import static java.sql.Types.VARCHAR; import static java.util.logging.Level.WARNING; -import static org.briarproject.api.identity.Author.Status.ANONYMOUS; -import static org.briarproject.api.identity.Author.Status.UNKNOWN; -import static org.briarproject.api.identity.Author.Status.VERIFIED; -import static org.briarproject.api.sync.MessagingConstants.MAX_SUBSCRIPTIONS; +import static org.briarproject.api.db.Metadata.REMOVE; +import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS; import static org.briarproject.db.ExponentialBackoff.calculateExpiry; /** @@ -63,8 +60,12 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry; */ abstract class JdbcDatabase implements Database<Connection> { - private static final int SCHEMA_VERSION = 12; - private static final int MIN_SCHEMA_VERSION = 12; + private static final int SCHEMA_VERSION = 14; + private static final int MIN_SCHEMA_VERSION = 14; + + private static final int VALIDATION_UNKNOWN = 0; + private static final int VALIDATION_INVALID = 1; + private static final int VALIDATION_VALID = 2; private static final String CREATE_SETTINGS = "CREATE TABLE settings" @@ -98,8 +99,8 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_GROUPS = "CREATE TABLE groups" + " (groupId HASH NOT NULL," - + " name VARCHAR NOT NULL," - + " salt BINARY NOT NULL," + + " clientId HASH NOT NULL," + + " descriptor BINARY NOT NULL," + " visibleToAll BOOLEAN NOT NULL," + " PRIMARY KEY (groupId))"; @@ -107,7 +108,6 @@ abstract class JdbcDatabase implements Database<Connection> { "CREATE TABLE groupVisibilities" + " (contactId INT NOT NULL," + " groupId HASH NOT NULL," - + " inbox BOOLEAN NOT NULL," + " PRIMARY KEY (contactId, groupId)," + " FOREIGN KEY (contactId)" + " REFERENCES contacts (contactId)" @@ -120,8 +120,8 @@ abstract class JdbcDatabase implements Database<Connection> { "CREATE TABLE contactGroups" + " (contactId INT NOT NULL," + " groupId HASH NOT NULL," // Not a foreign key - + " name VARCHAR NOT NULL," - + " salt BINARY NOT NULL," + + " clientId HASH NOT NULL," + + " descriptor BINARY NOT NULL," + " PRIMARY KEY (contactId, groupId)," + " FOREIGN KEY (contactId)" + " REFERENCES contacts (contactId)" @@ -144,26 +144,26 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_MESSAGES = "CREATE TABLE messages" + " (messageId HASH NOT NULL," - + " parentId HASH," // Null for the first msg in a thread + " groupId HASH NOT NULL," - + " authorId HASH," // Null for private/anon messages - + " authorName VARCHAR," // Null for private/anon messages - + " authorKey VARCHAR," // Null for private/anon messages - + " contentType VARCHAR NOT NULL," + " timestamp BIGINT NOT NULL," + + " local BOOLEAN NOT NULL," + + " valid INT NOT NULL," + " length INT NOT NULL," - + " bodyStart INT NOT NULL," - + " bodyLength INT NOT NULL," + " raw BLOB NOT NULL," - + " local BOOLEAN NOT NULL," - + " read BOOLEAN NOT NULL," + " PRIMARY KEY (messageId)," + " FOREIGN KEY (groupId)" + " REFERENCES groups (groupId)" + " ON DELETE CASCADE)"; - private static final String INDEX_MESSAGES_BY_TIMESTAMP = - "CREATE INDEX messagesByTimestamp ON messages (timestamp)"; + private static final String CREATE_MESSAGE_METADATA = + "CREATE TABLE messageMetadata" + + " (messageId HASH NOT NULL," + + " key VARCHAR NOT NULL," + + " value BINARY NOT NULL," + + " PRIMARY KEY (messageId, key)," + + " FOREIGN KEY (messageId)" + + " REFERENCES messages (messageId)" + + " ON DELETE CASCADE)"; private static final String CREATE_OFFERS = "CREATE TABLE offers" @@ -191,12 +191,6 @@ abstract class JdbcDatabase implements Database<Connection> { + " REFERENCES contacts (contactId)" + " ON DELETE CASCADE)"; - private static final String INDEX_STATUSES_BY_MESSAGE = - "CREATE INDEX statusesByMessage ON statuses (messageId)"; - - private static final String INDEX_STATUSES_BY_CONTACT = - "CREATE INDEX statusesByContact ON statuses (contactId)"; - private static final String CREATE_TRANSPORTS = "CREATE TABLE transports" + " (transportId VARCHAR NOT NULL," @@ -393,11 +387,9 @@ abstract class JdbcDatabase implements Database<Connection> { s.executeUpdate(insertTypeNames(CREATE_CONTACT_GROUPS)); s.executeUpdate(insertTypeNames(CREATE_GROUP_VERSIONS)); s.executeUpdate(insertTypeNames(CREATE_MESSAGES)); - s.executeUpdate(INDEX_MESSAGES_BY_TIMESTAMP); + s.executeUpdate(insertTypeNames(CREATE_MESSAGE_METADATA)); s.executeUpdate(insertTypeNames(CREATE_OFFERS)); s.executeUpdate(insertTypeNames(CREATE_STATUSES)); - s.executeUpdate(INDEX_STATUSES_BY_MESSAGE); - s.executeUpdate(INDEX_STATUSES_BY_CONTACT); s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_CONFIGS)); s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_PROPS)); @@ -596,9 +588,8 @@ abstract class JdbcDatabase implements Database<Connection> { rs.close(); ps.close(); if (!ids.isEmpty()) { - sql = "INSERT INTO groupVisibilities" - + " (contactId, groupId, inbox)" - + " VALUES (?, ?, FALSE)"; + sql = "INSERT INTO groupVisibilities (contactId, groupId)" + + " VALUES (?, ?)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); for (byte[] id : ids) { @@ -656,6 +647,40 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void addContactGroup(Connection txn, ContactId c, Group g) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT NULL FROM contactGroups" + + " WHERE contactId = ? AND groupId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setBytes(2, g.getId().getBytes()); + rs = ps.executeQuery(); + boolean found = rs.next(); + if (rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + if (found) return; + sql = "INSERT INTO contactGroups" + + " (contactId, groupId, clientId, descriptor)" + + " VALUES (?, ?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setInt(1, c.getInt()); + ps.setBytes(2, g.getId().getBytes()); + ps.setBytes(3, g.getClientId().getBytes()); + ps.setBytes(4, g.getDescriptor()); + int affected = ps.executeUpdate(); + if (affected != 1) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public boolean addGroup(Connection txn, Group g) throws DbException { PreparedStatement ps = null; ResultSet rs = null; @@ -671,12 +696,12 @@ abstract class JdbcDatabase implements Database<Connection> { if (count > MAX_SUBSCRIPTIONS) throw new DbStateException(); if (count == MAX_SUBSCRIPTIONS) return false; sql = "INSERT INTO groups" - + " (groupId, name, salt, visibleToAll)" + + " (groupId, clientId, descriptor, visibleToAll)" + " VALUES (?, ?, ?, FALSE)"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getId().getBytes()); - ps.setString(2, g.getName()); - ps.setBytes(3, g.getSalt()); + ps.setBytes(2, g.getClientId().getBytes()); + ps.setBytes(3, g.getDescriptor()); int affected = ps.executeUpdate(); if (affected != 1) throw new DbStateException(); ps.close(); @@ -714,34 +739,18 @@ abstract class JdbcDatabase implements Database<Connection> { throws DbException { PreparedStatement ps = null; try { - String sql = "INSERT INTO messages (messageId, parentId, groupId," - + " authorId, authorName, authorKey, contentType," - + " timestamp, length, bodyStart, bodyLength, raw," - + " local, read)" - + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, FALSE)"; + String sql = "INSERT INTO messages (messageId, groupId, timestamp," + + " local, valid, length, raw)" + + " VALUES (?, ?, ?, ?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getId().getBytes()); - if (m.getParent() == null) ps.setNull(2, BINARY); - else ps.setBytes(2, m.getParent().getBytes()); - ps.setBytes(3, m.getGroup().getId().getBytes()); - Author a = m.getAuthor(); - if (a == null) { - ps.setNull(4, BINARY); - ps.setNull(5, VARCHAR); - ps.setNull(6, BINARY); - } else { - ps.setBytes(4, a.getId().getBytes()); - ps.setString(5, a.getName()); - ps.setBytes(6, a.getPublicKey()); - } - ps.setString(7, m.getContentType()); - ps.setLong(8, m.getTimestamp()); - byte[] raw = m.getSerialised(); - ps.setInt(9, raw.length); - ps.setInt(10, m.getBodyStart()); - ps.setInt(11, m.getBodyLength()); - ps.setBytes(12, raw); - ps.setBoolean(13, local); + ps.setBytes(2, m.getGroupId().getBytes()); + ps.setLong(3, m.getTimestamp()); + ps.setBoolean(4, local); + ps.setInt(5, local ? VALIDATION_VALID : VALIDATION_UNKNOWN); + byte[] raw = m.getRaw(); + ps.setInt(6, raw.length); + ps.setBytes(7, raw); int affected = ps.executeUpdate(); if (affected != 1) throw new DbStateException(); ps.close(); @@ -924,9 +933,8 @@ abstract class JdbcDatabase implements Database<Connection> { throws DbException { PreparedStatement ps = null; try { - String sql = "INSERT INTO groupVisibilities" - + " (contactId, groupId, inbox)" - + " VALUES (?, ?, FALSE)"; + String sql = "INSERT INTO groupVisibilities (contactId, groupId)" + + " VALUES (?, ?)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); ps.setBytes(2, g.getBytes()); @@ -1147,27 +1155,28 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<Group> getAvailableGroups(Connection txn) + public Collection<Group> getAvailableGroups(Connection txn, ClientId c) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT DISTINCT cg.groupId, cg.name, cg.salt" + String sql = "SELECT DISTINCT cg.groupId, cg.descriptor" + " FROM contactGroups AS cg" + " LEFT OUTER JOIN groups AS g" + " ON cg.groupId = g.groupId" - + " WHERE g.groupId IS NULL" + + " WHERE cg.clientId = ?" + + " AND g.groupId IS NULL" + " GROUP BY cg.groupId"; ps = txn.prepareStatement(sql); + ps.setBytes(1, c.getBytes()); rs = ps.executeQuery(); List<Group> groups = new ArrayList<Group>(); Set<GroupId> ids = new HashSet<GroupId>(); while (rs.next()) { GroupId id = new GroupId(rs.getBytes(1)); if (!ids.add(id)) throw new DbStateException(); - String name = rs.getString(2); - byte[] salt = rs.getBytes(3); - groups.add(new Group(id, name, salt)); + byte[] descriptor = rs.getBytes(2); + groups.add(new Group(id, c, descriptor)); } rs.close(); ps.close(); @@ -1281,16 +1290,17 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT name, salt FROM groups WHERE groupId = ?"; + String sql = "SELECT clientId, descriptor FROM groups" + + " WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); rs = ps.executeQuery(); if (!rs.next()) throw new DbStateException(); - String name = rs.getString(1); - byte[] salt = rs.getBytes(2); + ClientId clientId = new ClientId(rs.getBytes(1)); + byte[] descriptor = rs.getBytes(2); rs.close(); ps.close(); - return new Group(g, name, salt); + return new Group(g, clientId, descriptor); } catch (SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1298,24 +1308,21 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Collection<Group> getGroups(Connection txn) throws DbException { + public Collection<Group> getGroups(Connection txn, ClientId c) + throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT DISTINCT g.groupId, name, salt" - + " FROM groups AS g" - + " LEFT OUTER JOIN groupVisibilities AS gv" - + " ON g.groupId = gv.groupId" - + " WHERE gv.inbox IS NULL OR gv.inbox = FALSE" - + " GROUP BY g.groupId"; + String sql = "SELECT groupId, descriptor FROM groups" + + " WHERE clientId = ?"; ps = txn.prepareStatement(sql); + ps.setBytes(1, c.getBytes()); rs = ps.executeQuery(); List<Group> groups = new ArrayList<Group>(); while (rs.next()) { GroupId id = new GroupId(rs.getBytes(1)); - String name = rs.getString(2); - byte[] salt = rs.getBytes(3); - groups.add(new Group(id, name, salt)); + byte[] descriptor = rs.getBytes(2); + groups.add(new Group(id, c, descriptor)); } rs.close(); ps.close(); @@ -1327,103 +1334,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public GroupId getInboxGroupId(Connection txn, ContactId c) - throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT groupId FROM groupVisibilities" - + " WHERE contactId = ?" - + " AND inbox = TRUE"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - rs = ps.executeQuery(); - GroupId inbox = null; - if (rs.next()) inbox = new GroupId(rs.getBytes(1)); - if (rs.next()) throw new DbStateException(); - rs.close(); - ps.close(); - return inbox; - } catch (SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - - public Collection<MessageHeader> getInboxMessageHeaders(Connection txn, - ContactId c) throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - // Get the local and remote authors - String sql = "SELECT la.authorId, la.name, la.publicKey," - + " c.authorId, c.name, c.publicKey" - + " FROM localAuthors AS la" - + " JOIN contacts AS c" - + " ON la.authorId = c.localAuthorId" - + " WHERE contactId = ?"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - rs = ps.executeQuery(); - if (!rs.next()) throw new DbException(); - AuthorId localId = new AuthorId(rs.getBytes(1)); - String localName = rs.getString(2); - byte[] localKey = rs.getBytes(3); - Author localAuthor = new Author(localId, localName, localKey); - AuthorId remoteId = new AuthorId(rs.getBytes(4)); - String remoteName = rs.getString(5); - byte[] remoteKey = rs.getBytes(6); - Author remoteAuthor = new Author(remoteId, remoteName, remoteKey); - if (rs.next()) throw new DbException(); - // Get the message headers - sql = "SELECT m.messageId, parentId, m.groupId, contentType," - + " timestamp, local, read, seen, s.txCount" - + " FROM messages AS m" - + " JOIN groups AS g" - + " ON m.groupId = g.groupId" - + " JOIN groupVisibilities AS gv" - + " ON m.groupId = gv.groupId" - + " JOIN statuses AS s" - + " ON m.messageId = s.messageId" - + " AND gv.contactId = s.contactId" - + " WHERE gv.contactId = ?" - + " AND inbox = TRUE"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - rs = ps.executeQuery(); - List<MessageHeader> headers = new ArrayList<MessageHeader>(); - while (rs.next()) { - MessageId id = new MessageId(rs.getBytes(1)); - byte[] b = rs.getBytes(2); - MessageId parent = b == null ? null : new MessageId(b); - GroupId groupId = new GroupId(rs.getBytes(3)); - String contentType = rs.getString(4); - long timestamp = rs.getLong(5); - boolean local = rs.getBoolean(6); - boolean read = rs.getBoolean(7); - boolean seen = rs.getBoolean(8); - Author author = local ? localAuthor : remoteAuthor; - - // initialize message status - State status; - if (seen) status = State.DELIVERED; - else if (rs.getInt(9) > 0) status = State.SENT; - else status = State.STORED; - - headers.add(new MessageHeader(id, parent, groupId, author, - VERIFIED, contentType, timestamp, local, read, status)); - } - rs.close(); - ps.close(); - return Collections.unmodifiableList(headers); - } catch (SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - public LocalAuthor getLocalAuthor(Connection txn, AuthorId a) throws DbException { PreparedStatement ps = null; @@ -1538,25 +1448,58 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public byte[] getMessageBody(Connection txn, MessageId m) + public Map<MessageId, Metadata> getMessageMetadata(Connection txn, + GroupId g) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT m.messageId, key, value" + + " FROM messages AS m" + + " JOIN messageMetadata AS md" + + " ON m.messageId = md.messageId" + + " WHERE groupId = ?" + + " ORDER BY m.messageId"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getBytes()); + rs = ps.executeQuery(); + Map<MessageId, Metadata> all = new HashMap<MessageId, Metadata>(); + Metadata metadata = null; + MessageId lastMessageId = null; + while (rs.next()) { + MessageId messageId = new MessageId(rs.getBytes(1)); + if (!messageId.equals(lastMessageId)) { + metadata = new Metadata(); + all.put(messageId, metadata); + lastMessageId = messageId; + } + metadata.put(rs.getString(2), rs.getBytes(3)); + } + rs.close(); + ps.close(); + return Collections.unmodifiableMap(all); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Metadata getMessageMetadata(Connection txn, MessageId m) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT bodyStart, bodyLength, raw FROM messages" + String sql = "SELECT key, value" + + " FROM messageMetadata" + " WHERE messageId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getBytes()); rs = ps.executeQuery(); - if (!rs.next()) throw new DbStateException(); - int bodyStart = rs.getInt(1); - int bodyLength = rs.getInt(2); - // Bytes are indexed from 1 rather than 0 - byte[] body = rs.getBlob(3).getBytes(bodyStart + 1, bodyLength); - if (rs.next()) throw new DbStateException(); + Metadata metadata = new Metadata(); + while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2)); rs.close(); ps.close(); - return body; + return metadata; } catch (SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1564,60 +1507,58 @@ abstract class JdbcDatabase implements Database<Connection> { } } - /** - * This method is used to get group messages. - * The message status won't be used. - */ - public Collection<MessageHeader> getMessageHeaders(Connection txn, - GroupId g) throws DbException { + public Collection<MessageStatus> getMessageStatus(Connection txn, + ContactId c, GroupId g) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT messageId, parentId, m.authorId, authorName," - + " authorKey, contentType, timestamp, local, read," - + " la.authorId IS NOT NULL, c.authorId IS NOT NULL" + String sql = "SELECT m.messageId, txCount > 0, seen" + " FROM messages AS m" - + " LEFT OUTER JOIN localAuthors AS la" - + " ON m.authorId = la.authorId" - + " LEFT OUTER JOIN contacts AS c" - + " ON m.authorId = c.authorId" - + " WHERE groupId = ?"; + + " JOIN statuses AS s" + + " ON m.messageId = s.messageId" + + " WHERE groupId = ?" + + " AND contactId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); + ps.setInt(2, c.getInt()); rs = ps.executeQuery(); - List<MessageHeader> headers = new ArrayList<MessageHeader>(); + List<MessageStatus> statuses = new ArrayList<MessageStatus>(); while (rs.next()) { - MessageId id = new MessageId(rs.getBytes(1)); - byte[] b = rs.getBytes(2); - MessageId parent = b == null ? null : new MessageId(b); - Author author; - b = rs.getBytes(3); - if (b == null) { - author = null; - } else { - AuthorId authorId = new AuthorId(b); - String authorName = rs.getString(4); - byte[] authorKey = rs.getBytes(5); - author = new Author(authorId, authorName, authorKey); - } - String contentType = rs.getString(6); - long timestamp = rs.getLong(7); - boolean local = rs.getBoolean(8); - boolean read = rs.getBoolean(9); - boolean isSelf = rs.getBoolean(10); - boolean isContact = rs.getBoolean(11); - - Author.Status status; - if (author == null) status = ANONYMOUS; - else if (isSelf || isContact) status = VERIFIED; - else status = UNKNOWN; - - headers.add(new MessageHeader(id, parent, g, author, status, - contentType, timestamp, local, read, State.STORED)); + MessageId messageId = new MessageId(rs.getBytes(1)); + boolean sent = rs.getBoolean(2); + boolean seen = rs.getBoolean(3); + statuses.add(new MessageStatus(messageId, c, sent, seen)); } rs.close(); ps.close(); - return Collections.unmodifiableList(headers); + return Collections.unmodifiableList(statuses); + } catch (SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public MessageStatus getMessageStatus(Connection txn, + ContactId c, MessageId m) throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT txCount > 0, seen" + + " FROM statuses" + + " WHERE messageId = ?" + + " AND contactId = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + ps.setInt(2, c.getInt()); + rs = ps.executeQuery(); + if (!rs.next()) throw new DbStateException(); + boolean sent = rs.getBoolean(1); + boolean seen = rs.getBoolean(2); + if (rs.next()) throw new DbStateException(); + rs.close(); + ps.close(); + return new MessageStatus(m, c, sent, seen); } catch (SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1665,13 +1606,15 @@ abstract class JdbcDatabase implements Database<Connection> { + " ON m.messageId = s.messageId" + " AND cg.contactId = s.contactId" + " WHERE cg.contactId = ?" + + " AND valid = ?" + " AND seen = FALSE AND requested = FALSE" + " AND s.expiry < ?" + " ORDER BY timestamp DESC LIMIT ?"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setLong(2, now); - ps.setInt(3, maxMessages); + ps.setInt(2, VALIDATION_VALID); + ps.setLong(3, now); + ps.setInt(4, maxMessages); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); @@ -1725,12 +1668,14 @@ abstract class JdbcDatabase implements Database<Connection> { + " ON m.messageId = s.messageId" + " AND cg.contactId = s.contactId" + " WHERE cg.contactId = ?" + + " AND valid = ?" + " AND seen = FALSE" + " AND s.expiry < ?" + " ORDER BY timestamp DESC"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setLong(2, now); + ps.setInt(2, VALIDATION_VALID); + ps.setLong(3, now); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); int total = 0; @@ -1750,26 +1695,23 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public MessageId getParent(Connection txn, MessageId m) throws DbException { + public Collection<MessageId> getMessagesToValidate(Connection txn, + ClientId c) throws DbException { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT m1.parentId FROM messages AS m1" - + " JOIN messages AS m2" - + " ON m1.parentId = m2.messageId" - + " AND m1.groupId = m2.groupId" - + " WHERE m1.messageId = ?"; + String sql = "SELECT messageId FROM messages AS m" + + " JOIN groups AS g ON m.groupId = g.groupId" + + " WHERE valid = ? AND clientId = ?"; ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); + ps.setInt(1, VALIDATION_UNKNOWN); + ps.setBytes(2, c.getBytes()); rs = ps.executeQuery(); - MessageId parent = null; - if (rs.next()) { - parent = new MessageId(rs.getBytes(1)); - if (rs.next()) throw new DbStateException(); - } + List<MessageId> ids = new ArrayList<MessageId>(); + while (rs.next()) ids.add(new MessageId(rs.getBytes(1))); rs.close(); ps.close(); - return parent; + return Collections.unmodifiableList(ids); } catch (SQLException e) { tryToClose(rs); tryToClose(ps); @@ -1782,14 +1724,12 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT length, raw FROM messages WHERE messageId = ?"; + String sql = "SELECT raw FROM messages WHERE messageId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, m.getBytes()); rs = ps.executeQuery(); if (!rs.next()) throw new DbStateException(); - int length = rs.getInt(1); - byte[] raw = rs.getBlob(2).getBytes(1, length); - if (raw.length != length) throw new DbStateException(); + byte[] raw = rs.getBytes(1); if (rs.next()) throw new DbStateException(); rs.close(); ps.close(); @@ -1801,27 +1741,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public boolean getReadFlag(Connection txn, MessageId m) throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT read FROM messages WHERE messageId = ?"; - ps = txn.prepareStatement(sql); - ps.setBytes(1, m.getBytes()); - rs = ps.executeQuery(); - if (!rs.next()) throw new DbStateException(); - boolean read = rs.getBoolean(1); - if (rs.next()) throw new DbStateException(); - rs.close(); - ps.close(); - return read; - } catch (SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - public Map<ContactId, TransportProperties> getRemoteProperties( Connection txn, TransportId t) throws DbException { PreparedStatement ps = null; @@ -1874,12 +1793,14 @@ abstract class JdbcDatabase implements Database<Connection> { + " ON m.messageId = s.messageId" + " AND cg.contactId = s.contactId" + " WHERE cg.contactId = ?" + + " AND valid = ?" + " AND seen = FALSE AND requested = TRUE" + " AND s.expiry < ?" + " ORDER BY timestamp DESC"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); - ps.setLong(2, now); + ps.setInt(2, VALIDATION_VALID); + ps.setLong(3, now); rs = ps.executeQuery(); List<MessageId> ids = new ArrayList<MessageId>(); int total = 0; @@ -1993,7 +1914,8 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT g.groupId, name, salt, localVersion, txCount" + String sql = "SELECT g.groupId, clientId, descriptor," + + " localVersion, txCount" + " FROM groups AS g" + " JOIN groupVisibilities AS gvis" + " ON g.groupId = gvis.groupId" @@ -2013,9 +1935,9 @@ abstract class JdbcDatabase implements Database<Connection> { while (rs.next()) { GroupId id = new GroupId(rs.getBytes(1)); if (!ids.add(id)) throw new DbStateException(); - String name = rs.getString(2); - byte[] salt = rs.getBytes(3); - groups.add(new Group(id, name, salt)); + ClientId clientId = new ClientId(rs.getBytes(2)); + byte[] descriptor = rs.getBytes(3); + groups.add(new Group(id, clientId, descriptor)); version = rs.getLong(4); txCount = rs.getInt(5); } @@ -2233,32 +2155,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public Map<GroupId, Integer> getUnreadMessageCounts(Connection txn) - throws DbException { - PreparedStatement ps = null; - ResultSet rs = null; - try { - String sql = "SELECT groupId, COUNT(*)" - + " FROM messages AS m" - + " WHERE read = FALSE" - + " GROUP BY groupId"; - ps = txn.prepareStatement(sql); - rs = ps.executeQuery(); - Map<GroupId, Integer> counts = new HashMap<GroupId, Integer>(); - while (rs.next()) { - GroupId groupId = new GroupId(rs.getBytes(1)); - counts.put(groupId, rs.getInt(2)); - } - rs.close(); - ps.close(); - return Collections.unmodifiableMap(counts); - } catch (SQLException e) { - tryToClose(rs); - tryToClose(ps); - throw new DbException(e); - } - } - public Collection<ContactId> getVisibility(Connection txn, GroupId g) throws DbException { PreparedStatement ps = null; @@ -2419,11 +2315,87 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void mergeMessageMetadata(Connection txn, MessageId m, Metadata meta) + throws DbException { + PreparedStatement ps = null; + try { + // Determine which keys are being removed + List<String> removed = new ArrayList<String>(); + Map<String, byte[]> retained = new HashMap<String, byte[]>(); + for (Entry<String, byte[]> e : meta.entrySet()) { + if (e.getValue() == REMOVE) removed.add(e.getKey()); + else retained.put(e.getKey(), e.getValue()); + } + // Delete any keys that are being removed + if (!removed.isEmpty()) { + String sql = "DELETE FROM messageMetadata" + + " WHERE messageId = ? AND key = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + for (String key : removed) { + ps.setString(2, key); + ps.addBatch(); + } + int[] batchAffected = ps.executeBatch(); + if (batchAffected.length != removed.size()) + throw new DbStateException(); + for (int i = 0; i < batchAffected.length; i++) { + if (batchAffected[i] < 0) throw new DbStateException(); + if (batchAffected[i] > 1) throw new DbStateException(); + } + ps.close(); + } + if (retained.isEmpty()) return; + // Update any keys that already exist + String sql = "UPDATE messageMetadata SET value = ?" + + " WHERE messageId = ? AND key = ?"; + ps = txn.prepareStatement(sql); + ps.setBytes(2, m.getBytes()); + for (Entry<String, byte[]> e : retained.entrySet()) { + ps.setBytes(1, e.getValue()); + ps.setString(3, e.getKey()); + ps.addBatch(); + } + int[] batchAffected = ps.executeBatch(); + if (batchAffected.length != retained.size()) + throw new DbStateException(); + for (int i = 0; i < batchAffected.length; i++) { + if (batchAffected[i] < 0) throw new DbStateException(); + if (batchAffected[i] > 1) throw new DbStateException(); + } + // Insert any keys that don't already exist + sql = "INSERT INTO messageMetadata (messageId, key, value)" + + " VALUES (?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, m.getBytes()); + int updateIndex = 0, inserted = 0; + for (Entry<String, byte[]> e : retained.entrySet()) { + if (batchAffected[updateIndex] == 0) { + ps.setString(2, e.getKey()); + ps.setBytes(3, e.getValue()); + ps.addBatch(); + inserted++; + } + updateIndex++; + } + batchAffected = ps.executeBatch(); + if (batchAffected.length != inserted) throw new DbStateException(); + for (int i = 0; i < batchAffected.length; i++) { + if (batchAffected[i] != 1) throw new DbStateException(); + } + ps.close(); + } catch (SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public void mergeSettings(Connection txn, Settings s, String namespace) throws DbException { PreparedStatement ps = null; try { // Update any settings that already exist - String sql = "UPDATE settings SET value = ? WHERE key = ? AND namespace = ?"; + String sql = "UPDATE settings SET value = ?" + + " WHERE key = ? AND namespace = ?"; ps = txn.prepareStatement(sql); for (Entry<String, String> e : s.entrySet()) { ps.setString(1, e.getValue()); @@ -2438,7 +2410,8 @@ abstract class JdbcDatabase implements Database<Connection> { if (batchAffected[i] > 1) throw new DbStateException(); } // Insert any settings that don't already exist - sql = "INSERT INTO settings (key, value, namespace) VALUES (?, ?, ?)"; + sql = "INSERT INTO settings (key, value, namespace)" + + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); int updateIndex = 0, inserted = 0; for (Entry<String, String> e : s.entrySet()) { @@ -2714,6 +2687,23 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void setMessageValidity(Connection txn, MessageId m, boolean valid) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "UPDATE messages SET valid = ? WHERE messageId = ?"; + ps = txn.prepareStatement(sql); + ps.setInt(1, valid ? VALIDATION_VALID : VALIDATION_INVALID); + ps.setBytes(2, m.getBytes()); + int affected = ps.executeUpdate(); + if (affected < 0) throw new DbStateException(); + ps.close(); + } catch (SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public void setReorderingWindow(Connection txn, ContactId c, TransportId t, long rotationPeriod, long base, byte[] bitmap) throws DbException { PreparedStatement ps = null; @@ -2798,14 +2788,14 @@ abstract class JdbcDatabase implements Database<Connection> { // Store the new subscriptions, if any if (groups.isEmpty()) return true; sql = "INSERT INTO contactGroups" - + " (contactId, groupId, name, salt)" + + " (contactId, groupId, clientId, descriptor)" + " VALUES (?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); for (Group g : groups) { ps.setBytes(2, g.getId().getBytes()); - ps.setString(3, g.getName()); - ps.setBytes(4, g.getSalt()); + ps.setBytes(3, g.getClientId().getBytes()); + ps.setBytes(4, g.getDescriptor()); ps.addBatch(); } int[] batchAffected = ps.executeBatch(); @@ -2823,66 +2813,6 @@ abstract class JdbcDatabase implements Database<Connection> { } } - public void setInboxGroup(Connection txn, ContactId c, Group g) - throws DbException { - PreparedStatement ps = null; - try { - // Unset any existing inbox group for the contact - String sql = "UPDATE groupVisibilities" - + " SET inbox = FALSE" - + " WHERE contactId = ?" - + " AND inbox = TRUE"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.executeUpdate(); - int affected = ps.executeUpdate(); - if (affected < 0 || affected > 1) throw new DbStateException(); - ps.close(); - // Make the group visible to the contact and set it as the inbox - sql = "INSERT INTO groupVisibilities" - + " (contactId, groupId, inbox)" - + " VALUES (?, ?, TRUE)"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.setBytes(2, g.getId().getBytes()); - affected = ps.executeUpdate(); - if (affected != 1) throw new DbStateException(); - ps.close(); - // Add the group to the contact's subscriptions - sql = "INSERT INTO contactGroups" - + " (contactId, groupId, name, salt)" - + " VALUES (?, ?, ?, ?)"; - ps = txn.prepareStatement(sql); - ps.setInt(1, c.getInt()); - ps.setBytes(2, g.getId().getBytes()); - ps.setString(3, g.getName()); - ps.setBytes(4, g.getSalt()); - affected = ps.executeUpdate(); - if (affected != 1) throw new DbStateException(); - ps.close(); - } catch (SQLException e) { - tryToClose(ps); - throw new DbException(e); - } - } - - public void setReadFlag(Connection txn, MessageId m, boolean read) - throws DbException { - PreparedStatement ps = null; - try { - String sql = "UPDATE messages SET read = ? WHERE messageId = ?"; - ps = txn.prepareStatement(sql); - ps.setBoolean(1, read); - ps.setBytes(2, m.getBytes()); - int affected = ps.executeUpdate(); - if (affected < 0 || affected > 1) throw new DbStateException(); - ps.close(); - } catch (SQLException e) { - tryToClose(ps); - throw new DbException(e); - } - } - public void setRemoteProperties(Connection txn, ContactId c, Map<TransportId, TransportProperties> p) throws DbException { PreparedStatement ps = null; diff --git a/briar-core/src/org/briarproject/forum/ForumFactoryImpl.java b/briar-core/src/org/briarproject/forum/ForumFactoryImpl.java deleted file mode 100644 index 3dcb401aa0cf1bec953f7b7e550a93ecdcf32577..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/forum/ForumFactoryImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.briarproject.forum; - -import com.google.inject.Inject; - -import org.briarproject.api.forum.Forum; -import org.briarproject.api.forum.ForumFactory; -import org.briarproject.api.sync.GroupFactory; - -// Temporary facade during sync protocol refactoring -class ForumFactoryImpl implements ForumFactory { - - private final GroupFactory groupFactory; - - @Inject - ForumFactoryImpl(GroupFactory groupFactory) { - this.groupFactory = groupFactory; - } - - public Forum createForum(String name) { - return new ForumImpl(groupFactory.createGroup(name)); - } -} diff --git a/briar-core/src/org/briarproject/forum/ForumImpl.java b/briar-core/src/org/briarproject/forum/ForumImpl.java deleted file mode 100644 index 90a6b85abfbc6d53609935fde2c8a51ba2c8f214..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/forum/ForumImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.briarproject.forum; - -import org.briarproject.api.forum.Forum; -import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.GroupId; - -// Temporary facade during sync protocol refactoring -class ForumImpl implements Forum { - - private final Group group; - - ForumImpl(Group group) { - this.group = group; - } - - public GroupId getId() { - return group.getId(); - } - - public String getName() { - return group.getName(); - } - - Group getGroup() { - return group; - } - - @Override - public int hashCode() { - return group.hashCode(); - } - - @Override - public boolean equals(Object o) { - return o instanceof ForumImpl && group.equals(((ForumImpl) o).group); - } -} diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java index c78badda84530e6601fc691312cc2e99dc127789..ba604e93809ff82de3d26a2503268bcbe3834c62 100644 --- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java +++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java @@ -2,79 +2,272 @@ package org.briarproject.forum; import com.google.inject.Inject; +import org.briarproject.api.FormatException; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.data.MetadataParser; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; import org.briarproject.api.forum.Forum; import org.briarproject.api.forum.ForumManager; +import org.briarproject.api.forum.ForumPost; import org.briarproject.api.forum.ForumPostHeader; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.GroupId; -import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageHeader; import org.briarproject.api.sync.MessageId; +import org.briarproject.util.StringUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; +import static org.briarproject.api.identity.Author.Status.ANONYMOUS; +import static org.briarproject.api.identity.Author.Status.UNKNOWN; +import static org.briarproject.api.identity.Author.Status.VERIFIED; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -// Temporary facade during sync protocol refactoring class ForumManagerImpl implements ForumManager { + static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString( + "859a7be50dca035b64bd6902fb797097" + + "795af837abbf8c16d750b3c2ccc186ea")); + + private static final Logger LOG = + Logger.getLogger(ForumManagerImpl.class.getName()); + private final DatabaseComponent db; + private final GroupFactory groupFactory; + private final BdfReaderFactory bdfReaderFactory; + private final BdfWriterFactory bdfWriterFactory; + private final MetadataEncoder metadataEncoder; + private final MetadataParser metadataParser; + private final SecureRandom random; @Inject - ForumManagerImpl(DatabaseComponent db) { + ForumManagerImpl(CryptoComponent crypto, DatabaseComponent db, + GroupFactory groupFactory, BdfReaderFactory bdfReaderFactory, + BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder, + MetadataParser metadataParser) { this.db = db; + this.groupFactory = groupFactory; + this.bdfReaderFactory = bdfReaderFactory; + this.bdfWriterFactory = bdfWriterFactory; + this.metadataEncoder = metadataEncoder; + this.metadataParser = metadataParser; + random = crypto.getSecureRandom(); + } + + @Override + public ClientId getClientId() { + return CLIENT_ID; + } + + @Override + public Forum createForum(String name) { + int length = StringUtils.toUtf8(name).length; + if (length == 0) throw new IllegalArgumentException(); + if (length > MAX_FORUM_NAME_LENGTH) + throw new IllegalArgumentException(); + byte[] salt = new byte[FORUM_SALT_LENGTH]; + random.nextBytes(salt); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + try { + w.writeListStart(); + w.writeString(name); + w.writeRaw(salt); + w.writeListEnd(); + } catch (IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(e); + } + Group g = groupFactory.createGroup(CLIENT_ID, out.toByteArray()); + return new Forum(g, name); } @Override public boolean addForum(Forum f) throws DbException { - return db.addGroup(((ForumImpl) f).getGroup()); + return db.addGroup(f.getGroup()); } @Override - public void addLocalPost(Message m) throws DbException { - db.addLocalMessage(m); + public void addLocalPost(ForumPost p) throws DbException { + BdfDictionary d = new BdfDictionary(); + d.put("timestamp", p.getMessage().getTimestamp()); + if (p.getParent() != null) d.put("parent", p.getParent().getBytes()); + if (p.getAuthor() != null) { + Author a = p.getAuthor(); + BdfDictionary d1 = new BdfDictionary(); + d1.put("id", a.getId().getBytes()); + d1.put("name", a.getName()); + d1.put("publicKey", a.getPublicKey()); + d.put("author", d1); + } + d.put("contentType", p.getContentType()); + d.put("local", true); + d.put("read", true); + try { + Metadata meta = metadataEncoder.encode(d); + db.addLocalMessage(p.getMessage(), CLIENT_ID, meta); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } } @Override public Collection<Forum> getAvailableForums() throws DbException { - Collection<Group> groups = db.getAvailableGroups(); + Collection<Group> groups = db.getAvailableGroups(CLIENT_ID); List<Forum> forums = new ArrayList<Forum>(groups.size()); - for (Group g : groups) forums.add(new ForumImpl(g)); + for (Group g : groups) { + try { + forums.add(parseForum(g)); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } return Collections.unmodifiableList(forums); } + private Forum parseForum(Group g) throws FormatException { + ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor()); + BdfReader r = bdfReaderFactory.createReader(in); + try { + r.readListStart(); + String name = r.readString(MAX_FORUM_NAME_LENGTH); + if (name.length() == 0) throw new FormatException(); + byte[] salt = r.readRaw(FORUM_SALT_LENGTH); + if (salt.length != FORUM_SALT_LENGTH) throw new FormatException(); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + return new Forum(g, name); + } catch (FormatException e) { + throw e; + } catch (IOException e) { + // Shouldn't happen with ByteArrayInputStream + throw new RuntimeException(e); + } + } + @Override public Forum getForum(GroupId g) throws DbException { - return new ForumImpl(db.getGroup(g)); + Group group = db.getGroup(g); + if (!group.getClientId().equals(CLIENT_ID)) + throw new IllegalArgumentException(); + try { + return parseForum(group); + } catch (FormatException e) { + throw new IllegalArgumentException(); + } } @Override public Collection<Forum> getForums() throws DbException { - Collection<Group> groups = db.getGroups(); + Collection<Group> groups = db.getGroups(CLIENT_ID); List<Forum> forums = new ArrayList<Forum>(groups.size()); - for (Group g : groups) forums.add(new ForumImpl(g)); + for (Group g : groups) { + try { + forums.add(parseForum(g)); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } return Collections.unmodifiableList(forums); } @Override public byte[] getPostBody(MessageId m) throws DbException { - return db.getMessageBody(m); + byte[] raw = db.getRawMessage(m); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); + try { + // Extract the forum post body + r.readListStart(); + if (r.hasRaw()) r.skipRaw(); // Parent ID + else r.skipNull(); // No parent + if (r.hasList()) r.skipList(); // Author + else r.skipNull(); // No author + r.skipString(); // Content type + return r.readRaw(MAX_FORUM_POST_BODY_LENGTH); + } catch (FormatException e) { + // Not a valid forum post + throw new IllegalArgumentException(); + } catch (IOException e) { + // Shouldn't happen with ByteArrayInputStream + throw new RuntimeException(e); + } } @Override public Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException { - Collection<MessageHeader> headers = db.getMessageHeaders(g); - List<ForumPostHeader> postHeaders = - new ArrayList<ForumPostHeader>(headers.size()); - for (MessageHeader m : headers) - postHeaders.add(new ForumPostHeaderImpl(m)); - return Collections.unmodifiableList(postHeaders); + // Load the IDs of the user's own identities and contacts' identities + Set<AuthorId> localAuthorIds = new HashSet<AuthorId>(); + for (LocalAuthor a : db.getLocalAuthors()) + localAuthorIds.add(a.getId()); + Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>(); + for (Contact c : db.getContacts()) + contactAuthorIds.add(c.getAuthor().getId()); + // Load and parse the metadata + Map<MessageId, Metadata> metadata = db.getMessageMetadata(g); + Collection<ForumPostHeader> headers = new ArrayList<ForumPostHeader>(); + for (Entry<MessageId, Metadata> e : metadata.entrySet()) { + MessageId messageId = e.getKey(); + Metadata meta = e.getValue(); + try { + BdfDictionary d = metadataParser.parse(meta); + long timestamp = d.getInteger("timestamp"); + Author author = null; + Author.Status authorStatus = ANONYMOUS; + BdfDictionary d1 = d.getDictionary("author", null); + if (d1 != null) { + AuthorId authorId = new AuthorId(d1.getRaw("id")); + String name = d1.getString("name"); + byte[] publicKey = d1.getRaw("publicKey"); + author = new Author(authorId, name, publicKey); + if (localAuthorIds.contains(authorId)) + authorStatus = VERIFIED; + else if (contactAuthorIds.contains(authorId)) + authorStatus = VERIFIED; + else authorStatus = UNKNOWN; + } + String contentType = d.getString("contentType"); + boolean read = d.getBoolean("read"); + headers.add(new ForumPostHeader(messageId, timestamp, author, + authorStatus, contentType, read)); + } catch (FormatException ex) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, ex.toString(), ex); + } + } + return headers; } @Override @@ -89,12 +282,18 @@ class ForumManagerImpl implements ForumManager { @Override public void removeForum(Forum f) throws DbException { - db.removeGroup(((ForumImpl) f).getGroup()); + db.removeGroup(f.getGroup()); } @Override public void setReadFlag(MessageId m, boolean read) throws DbException { - db.setReadFlag(m, read); + BdfDictionary d = new BdfDictionary(); + d.put("read", read); + try { + db.mergeMessageMetadata(m, metadataEncoder.encode(d)); + } catch (FormatException e) { + throw new RuntimeException(e); + } } @Override diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java index 6e63a34e88bb8cee12f5261b86e2f5c3a28b6645..60ac05bbe641f3f5efda11782c3adc0668627cae 100644 --- a/briar-core/src/org/briarproject/forum/ForumModule.java +++ b/briar-core/src/org/briarproject/forum/ForumModule.java @@ -1,17 +1,41 @@ package org.briarproject.forum; import com.google.inject.AbstractModule; +import com.google.inject.Provides; -import org.briarproject.api.forum.ForumFactory; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.data.ObjectReader; import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostFactory; +import org.briarproject.api.identity.Author; +import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.api.system.Clock; + +import javax.inject.Singleton; public class ForumModule extends AbstractModule { @Override protected void configure() { - bind(ForumFactory.class).to(ForumFactoryImpl.class); bind(ForumManager.class).to(ForumManagerImpl.class); bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class); } + + @Provides @Singleton + ForumPostValidator getValidator(LifecycleManager lifecycleManager, + CryptoComponent crypto, ValidationManager validationManager, + BdfReaderFactory bdfReaderFactory, + BdfWriterFactory bdfWriterFactory, + ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder, + Clock clock) { + ForumPostValidator validator = new ForumPostValidator(crypto, + validationManager, bdfReaderFactory, bdfWriterFactory, + authorReader, metadataEncoder, clock); + lifecycleManager.register(validator); + return validator; + } } diff --git a/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java b/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java index b4f09ce0f25719fd44180d704562a21cb13816f4..515310aa8fed400569f11b816e972101d5a89a76 100644 --- a/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java +++ b/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java @@ -1,43 +1,114 @@ package org.briarproject.forum; -import com.google.inject.Inject; - +import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.PrivateKey; -import org.briarproject.api.forum.Forum; +import org.briarproject.api.crypto.Signature; +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.forum.ForumPost; import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageFactory; import org.briarproject.api.sync.MessageId; +import org.briarproject.util.StringUtils; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; -// Temporary facade during sync protocol refactoring +import javax.inject.Inject; + +import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; + class ForumPostFactoryImpl implements ForumPostFactory { + private final CryptoComponent crypto; private final MessageFactory messageFactory; + private final BdfWriterFactory bdfWriterFactory; @Inject - ForumPostFactoryImpl(MessageFactory messageFactory) { + ForumPostFactoryImpl(CryptoComponent crypto, MessageFactory messageFactory, + BdfWriterFactory bdfWriterFactory) { + this.crypto = crypto; this.messageFactory = messageFactory; + this.bdfWriterFactory = bdfWriterFactory; } @Override - public Message createAnonymousPost(MessageId parent, Forum forum, - String contentType, long timestamp, byte[] body) + public ForumPost createAnonymousPost(GroupId groupId, long timestamp, + MessageId parent, String contentType, byte[] body) throws IOException, GeneralSecurityException { - return messageFactory.createAnonymousMessage(parent, - ((ForumImpl) forum).getGroup(), contentType, timestamp, body); + // Validate the arguments + if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH) + throw new IllegalArgumentException(); + if (body.length > MAX_FORUM_POST_BODY_LENGTH) + throw new IllegalArgumentException(); + // Serialise the message to a buffer + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + w.writeListStart(); + if (parent == null) w.writeNull(); + else w.writeRaw(parent.getBytes()); + w.writeNull(); // No author + w.writeString(contentType); + w.writeRaw(body); + w.writeNull(); // No signature + w.writeListEnd(); + Message m = messageFactory.createMessage(groupId, timestamp, + out.toByteArray()); + return new ForumPost(m, parent, null, contentType); } @Override - public Message createPseudonymousPost(MessageId parent, Forum forum, - Author author, PrivateKey privateKey, String contentType, - long timestamp, byte[] body) - throws IOException, GeneralSecurityException { - return messageFactory.createPseudonymousMessage(parent, - ((ForumImpl) forum).getGroup(), author, privateKey, contentType, - timestamp, body); + public ForumPost createPseudonymousPost(GroupId groupId, long timestamp, + MessageId parent, Author author, String contentType, byte[] body, + PrivateKey privateKey) throws IOException, + GeneralSecurityException { + // Validate the arguments + if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH) + throw new IllegalArgumentException(); + if (body.length > MAX_FORUM_POST_BODY_LENGTH) + throw new IllegalArgumentException(); + // Serialise the data to be signed + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + w.writeListStart(); + w.writeRaw(groupId.getBytes()); + w.writeInteger(timestamp); + if (parent == null) w.writeNull(); + else w.writeRaw(parent.getBytes()); + writeAuthor(w, author); + w.writeString(contentType); + w.writeRaw(body); + w.writeListEnd(); + // Generate the signature + Signature signature = crypto.getSignature(); + signature.initSign(privateKey); + signature.update(out.toByteArray()); + byte[] sig = signature.sign(); + // Serialise the signed message + out.reset(); + w = bdfWriterFactory.createWriter(out); + w.writeListStart(); + if (parent == null) w.writeNull(); + else w.writeRaw(parent.getBytes()); + writeAuthor(w, author); + w.writeString(contentType); + w.writeRaw(body); + w.writeRaw(sig); + w.writeListEnd(); + Message m = messageFactory.createMessage(groupId, timestamp, + out.toByteArray()); + return new ForumPost(m, parent, author, contentType); + } + + private void writeAuthor(BdfWriter w, Author a) throws IOException { + w.writeListStart(); + w.writeString(a.getName()); + w.writeRaw(a.getPublicKey()); + w.writeListEnd(); } } diff --git a/briar-core/src/org/briarproject/forum/ForumPostHeaderImpl.java b/briar-core/src/org/briarproject/forum/ForumPostHeaderImpl.java deleted file mode 100644 index 5aeaa5dece8781332697668d2eae7c976402a804..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/forum/ForumPostHeaderImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.briarproject.forum; - -import org.briarproject.api.forum.ForumPostHeader; -import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.MessageHeader; -import org.briarproject.api.sync.MessageId; - -// Temporary facade during sync protocol refactoring -class ForumPostHeaderImpl implements ForumPostHeader { - - private final MessageHeader messageHeader; - - ForumPostHeaderImpl(MessageHeader messageHeader) { - this.messageHeader = messageHeader; - } - - @Override - public MessageId getId() { - return messageHeader.getId(); - } - - @Override - public Author getAuthor() { - return messageHeader.getAuthor(); - } - - @Override - public Author.Status getAuthorStatus() { - return messageHeader.getAuthorStatus(); - } - - @Override - public String getContentType() { - return messageHeader.getContentType(); - } - - @Override - public long getTimestamp() { - return messageHeader.getTimestamp(); - } - - @Override - public boolean isRead() { - return messageHeader.isRead(); - } -} diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..205b290f478ca8589f0c683f9e7b56e7bb0c74eb --- /dev/null +++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java @@ -0,0 +1,184 @@ +package org.briarproject.forum; + +import org.briarproject.api.FormatException; +import org.briarproject.api.UniqueId; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyParser; +import org.briarproject.api.crypto.PublicKey; +import org.briarproject.api.crypto.Signature; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.data.ObjectReader; +import org.briarproject.api.db.Metadata; +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageValidator; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.api.system.Clock; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; +import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; +import static org.briarproject.forum.ForumManagerImpl.CLIENT_ID; + +class ForumPostValidator implements MessageValidator { + + private static final Logger LOG = + Logger.getLogger(ForumPostValidator.class.getName()); + + private final CryptoComponent crypto; + private final ValidationManager validationManager; + private final BdfReaderFactory bdfReaderFactory; + private final BdfWriterFactory bdfWriterFactory; + private final ObjectReader<Author> authorReader; + private final MetadataEncoder metadataEncoder; + private final Clock clock; + private final KeyParser keyParser; + + @Inject + ForumPostValidator(CryptoComponent crypto, + ValidationManager validationManager, + BdfReaderFactory bdfReaderFactory, + BdfWriterFactory bdfWriterFactory, + ObjectReader<Author> authorReader, + MetadataEncoder metadataEncoder, Clock clock) { + this.crypto = crypto; + this.validationManager = validationManager; + this.bdfReaderFactory = bdfReaderFactory; + this.bdfWriterFactory = bdfWriterFactory; + this.authorReader = authorReader; + this.metadataEncoder = metadataEncoder; + this.clock = clock; + keyParser = crypto.getSignatureKeyParser(); + } + + @Override + public boolean start() { + validationManager.setMessageValidator(CLIENT_ID, this); + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public Metadata validateMessage(Message m) { + // Reject the message if it's too far in the future + long now = clock.currentTimeMillis(); + if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { + LOG.info("Timestamp is too far in the future"); + return null; + } + try { + // Parse the message body + byte[] raw = m.getRaw(); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); + MessageId parent = null; + Author author = null; + String contentType; + byte[] postBody, sig = null; + r.readListStart(); + // Read the parent ID, if any + if (r.hasRaw()) { + byte[] id = r.readRaw(UniqueId.LENGTH); + if (id.length < UniqueId.LENGTH) throw new FormatException(); + parent = new MessageId(id); + } else { + r.readNull(); + } + // Read the author, if any + if (r.hasList()) author = authorReader.readObject(r); + else r.readNull(); + // Read the content type + contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); + // Read the forum post body + postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH); + + // Read the signature, if any + if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH); + else r.readNull(); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + // If there's an author there must be a signature and vice versa + if (author != null && sig == null) { + LOG.info("Author without signature"); + return null; + } + if (author == null && sig != null) { + LOG.info("Signature without author"); + return null; + } + // Verify the signature, if any + if (author != null) { + // Parse the public key + PublicKey key = keyParser.parsePublicKey(author.getPublicKey()); + // Serialise the data to be signed + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + w.writeListStart(); + w.writeRaw(m.getGroupId().getBytes()); + w.writeInteger(m.getTimestamp()); + if (parent == null) w.writeNull(); + else w.writeRaw(parent.getBytes()); + writeAuthor(w, author); + w.writeString(contentType); + w.writeRaw(postBody); + w.writeListEnd(); + // Verify the signature + Signature signature = crypto.getSignature(); + signature.initVerify(key); + signature.update(out.toByteArray()); + if (!signature.verify(sig)) { + LOG.info("Invalid signature"); + return null; + } + } + // Return the metadata + BdfDictionary d = new BdfDictionary(); + d.put("timestamp", m.getTimestamp()); + if (parent != null) d.put("parent", parent.getBytes()); + if (author != null) { + BdfDictionary d1 = new BdfDictionary(); + d1.put("id", author.getId().getBytes()); + d1.put("name", author.getName()); + d1.put("publicKey", author.getPublicKey()); + d.put("author", d1); + } + d.put("contentType", contentType); + d.put("read", false); + return metadataEncoder.encode(d); + } catch (IOException e) { + LOG.info("Invalid forum post"); + return null; + } catch (GeneralSecurityException e) { + LOG.info("Invalid public key"); + return null; + } + } + + private void writeAuthor(BdfWriter w, Author a) throws IOException { + w.writeListStart(); + w.writeString(a.getName()); + w.writeRaw(a.getPublicKey()); + w.writeListEnd(); + } +} diff --git a/briar-core/src/org/briarproject/invitation/Connector.java b/briar-core/src/org/briarproject/invitation/Connector.java index bd2d8a32545b5e57cb6437953cc9013fbe4e86ce..deb68a9d8ad12a7340e285b3e1c6b6bb324ba1a5 100644 --- a/briar-core/src/org/briarproject/invitation/Connector.java +++ b/briar-core/src/org/briarproject/invitation/Connector.java @@ -282,13 +282,13 @@ abstract class Connector extends Thread { // Add the contact to the database contactId = contactManager.addContact(remoteAuthor, localAuthor.getId()); - // Create a private messaging conversation - messagingManager.addContact(contactId, master); // Store the remote transport properties transportPropertyManager.setRemoteProperties(contactId, remoteProps); // Derive transport keys for each transport shared with the contact keyManager.addContact(contactId, remoteProps.keySet(), master, timestamp, alice); + // Create a private messaging conversation + messagingManager.addContact(contactId); } protected void tryToClose(DuplexTransportConnection conn, diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java index 712254c206f6039a01196c2af71bab0bc58153b0..39ba71647e09b77b985d94a39b9b0b777f1e6a7d 100644 --- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java +++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java @@ -2,88 +2,205 @@ package org.briarproject.messaging; import com.google.inject.Inject; +import org.briarproject.api.FormatException; +import org.briarproject.api.UniqueId; +import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.data.MetadataParser; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; +import org.briarproject.api.db.NoSuchContactException; +import org.briarproject.api.identity.AuthorId; import org.briarproject.api.messaging.MessagingManager; -import org.briarproject.api.messaging.PrivateConversation; +import org.briarproject.api.messaging.PrivateMessage; import org.briarproject.api.messaging.PrivateMessageHeader; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.GroupId; -import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageHeader; import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageStatus; +import org.briarproject.util.StringUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -// Temporary facade during sync protocol refactoring class MessagingManagerImpl implements MessagingManager { + static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString( + "6bcdc006c0910b0f44e40644c3b31f1a" + + "8bf9a6d6021d40d219c86b731b903070")); + + private static final Logger LOG = + Logger.getLogger(MessagingManagerImpl.class.getName()); + private final DatabaseComponent db; - private final CryptoComponent crypto; private final GroupFactory groupFactory; + private final BdfReaderFactory bdfReaderFactory; + private final BdfWriterFactory bdfWriterFactory; + private final MetadataEncoder metadataEncoder; + private final MetadataParser metadataParser; @Inject - MessagingManagerImpl(DatabaseComponent db, CryptoComponent crypto, - GroupFactory groupFactory) { + MessagingManagerImpl(DatabaseComponent db, GroupFactory groupFactory, + BdfReaderFactory bdfReaderFactory, + BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder, + MetadataParser metadataParser) { this.db = db; - this.crypto = crypto; this.groupFactory = groupFactory; + this.bdfReaderFactory = bdfReaderFactory; + this.bdfWriterFactory = bdfWriterFactory; + this.metadataEncoder = metadataEncoder; + this.metadataParser = metadataParser; } @Override - public void addContact(ContactId c, SecretKey master) throws DbException { - byte[] salt = crypto.deriveGroupSalt(master); - Group inbox = groupFactory.createGroup("Inbox", salt); - db.addGroup(inbox); - db.setInboxGroup(c, inbox); + public ClientId getClientId() { + return CLIENT_ID; + } + + @Override + public void addContact(ContactId c) throws DbException { + // Create the conversation group + Group conversation = createConversationGroup(db.getContact(c)); + // Subscribe to the group and share it with the contact + db.addGroup(conversation); + db.addContactGroup(c, conversation); + db.setVisibility(conversation.getId(), Collections.singletonList(c)); + } + + private Group createConversationGroup(Contact c) { + AuthorId local = c.getLocalAuthorId(); + AuthorId remote = c.getAuthor().getId(); + byte[] descriptor = createGroupDescriptor(local, remote); + return groupFactory.createGroup(CLIENT_ID, descriptor); + } + + private byte[] createGroupDescriptor(AuthorId local, AuthorId remote) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + try { + w.writeListStart(); + if (UniqueId.IdComparator.INSTANCE.compare(local, remote) < 0) { + w.writeRaw(local.getBytes()); + w.writeRaw(remote.getBytes()); + } else { + w.writeRaw(remote.getBytes()); + w.writeRaw(local.getBytes()); + } + w.writeListEnd(); + } catch (IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(e); + } + return out.toByteArray(); } @Override - public void addLocalMessage(Message m) throws DbException { - db.addLocalMessage(m); + public void addLocalMessage(PrivateMessage m) throws DbException { + BdfDictionary d = new BdfDictionary(); + d.put("timestamp", m.getMessage().getTimestamp()); + if (m.getParent() != null) d.put("parent", m.getParent().getBytes()); + d.put("contentType", m.getContentType()); + d.put("local", true); + d.put("read", true); + try { + Metadata meta = metadataEncoder.encode(d); + db.addLocalMessage(m.getMessage(), CLIENT_ID, meta); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } } @Override - public PrivateConversation getConversation(GroupId g) throws DbException { - return new PrivateConversationImpl(db.getGroup(g)); + public ContactId getContactId(GroupId g) throws DbException { + // TODO: Make this more efficient + for (Contact c : db.getContacts()) { + Group conversation = createConversationGroup(c); + if (conversation.getId().equals(g)) return c.getId(); + } + throw new NoSuchContactException(); } @Override public GroupId getConversationId(ContactId c) throws DbException { - return db.getInboxGroupId(c); + return createConversationGroup(db.getContact(c)).getId(); } @Override public Collection<PrivateMessageHeader> getMessageHeaders(ContactId c) throws DbException { - Collection<MessageHeader> headers = db.getInboxMessageHeaders(c); - List<PrivateMessageHeader> privateHeaders = - new ArrayList<PrivateMessageHeader>(headers.size()); - for (MessageHeader m : headers) - privateHeaders.add(new PrivateMessageHeaderImpl(m)); - return Collections.unmodifiableList(privateHeaders); + GroupId groupId = getConversationId(c); + Map<MessageId, Metadata> metadata = db.getMessageMetadata(groupId); + Collection<MessageStatus> statuses = db.getMessageStatus(c, groupId); + Collection<PrivateMessageHeader> headers = + new ArrayList<PrivateMessageHeader>(); + for (MessageStatus s : statuses) { + MessageId id = s.getMessageId(); + Metadata m = metadata.get(id); + if (m == null) continue; + try { + BdfDictionary d = metadataParser.parse(m); + long timestamp = d.getInteger("timestamp"); + String contentType = d.getString("contentType"); + boolean local = d.getBoolean("local"); + boolean read = d.getBoolean("read"); + headers.add(new PrivateMessageHeader(id, timestamp, contentType, + local, read, s.isSent(), s.isSeen())); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + return headers; } @Override public byte[] getMessageBody(MessageId m) throws DbException { - return db.getMessageBody(m); - } - - @Override - public void setConversation(ContactId c, PrivateConversation p) - throws DbException { - db.setInboxGroup(c, ((PrivateConversationImpl) p).getGroup()); + byte[] raw = db.getRawMessage(m); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); + try { + // Extract the private message body + r.readListStart(); + if (r.hasRaw()) r.skipRaw(); // Parent ID + else r.skipNull(); // No parent + r.skipString(); // Content type + return r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH); + } catch (FormatException e) { + // Not a valid private message + throw new IllegalArgumentException(); + } catch (IOException e) { + // Shouldn't happen with ByteArrayInputStream + throw new RuntimeException(e); + } } @Override public void setReadFlag(MessageId m, boolean read) throws DbException { - db.setReadFlag(m, read); + BdfDictionary d = new BdfDictionary(); + d.put("read", read); + try { + db.mergeMessageMetadata(m, metadataEncoder.encode(d)); + } catch (FormatException e) { + throw new RuntimeException(e); + } } } diff --git a/briar-core/src/org/briarproject/messaging/MessagingModule.java b/briar-core/src/org/briarproject/messaging/MessagingModule.java index 6134939932f75da6f1049aa589a547a680ee6bef..95a28a3f130c638c3dab0af04dedd470fd8938d5 100644 --- a/briar-core/src/org/briarproject/messaging/MessagingModule.java +++ b/briar-core/src/org/briarproject/messaging/MessagingModule.java @@ -1,9 +1,17 @@ package org.briarproject.messaging; import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.PrivateMessageFactory; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.api.system.Clock; + +import javax.inject.Singleton; public class MessagingModule extends AbstractModule { @@ -12,4 +20,15 @@ public class MessagingModule extends AbstractModule { bind(MessagingManager.class).to(MessagingManagerImpl.class); bind(PrivateMessageFactory.class).to(PrivateMessageFactoryImpl.class); } + + @Provides @Singleton + PrivateMessageValidator getValidator(LifecycleManager lifecycleManager, + ValidationManager validationManager, + BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder, + Clock clock) { + PrivateMessageValidator validator = new PrivateMessageValidator( + validationManager, bdfReaderFactory, metadataEncoder, clock); + lifecycleManager.register(validator); + return validator; + } } diff --git a/briar-core/src/org/briarproject/messaging/PrivateConversationImpl.java b/briar-core/src/org/briarproject/messaging/PrivateConversationImpl.java deleted file mode 100644 index febb0308c76438076895c739945135ed880cf8b7..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/messaging/PrivateConversationImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.briarproject.messaging; - -import org.briarproject.api.messaging.PrivateConversation; -import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.GroupId; - -// Temporary facade during sync protocol refactoring -class PrivateConversationImpl implements PrivateConversation { - - private final Group group; - - PrivateConversationImpl(Group group) { - this.group = group; - } - - @Override - public GroupId getId() { - return group.getId(); - } - - Group getGroup() { - return group; - } - - @Override - public int hashCode() { - return group.hashCode(); - } - - @Override - public boolean equals(Object o) { - return o instanceof PrivateConversationImpl - && group.equals(((PrivateConversationImpl) o).group); - } -} diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java b/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java index 71098bd41a254229e71e05b1a6ad1d8862b15b77..da8112c95f8b7cb9f02fc394504cc3124e57850e 100644 --- a/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java +++ b/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java @@ -1,33 +1,56 @@ package org.briarproject.messaging; -import com.google.inject.Inject; - -import org.briarproject.api.messaging.PrivateConversation; +import org.briarproject.api.data.BdfWriter; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.messaging.PrivateMessage; import org.briarproject.api.messaging.PrivateMessageFactory; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageFactory; import org.briarproject.api.sync.MessageId; +import org.briarproject.util.StringUtils; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; -// Temporary facade during sync protocol refactoring +import javax.inject.Inject; + +import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH; +import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH; + class PrivateMessageFactoryImpl implements PrivateMessageFactory { private final MessageFactory messageFactory; + private final BdfWriterFactory bdfWriterFactory; @Inject - PrivateMessageFactoryImpl(MessageFactory messageFactory) { + PrivateMessageFactoryImpl(MessageFactory messageFactory, + BdfWriterFactory bdfWriterFactory) { this.messageFactory = messageFactory; + this.bdfWriterFactory = bdfWriterFactory; } @Override - public Message createPrivateMessage(MessageId parent, - PrivateConversation conversation, String contentType, - long timestamp, byte[] body) + public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, + MessageId parent, String contentType, byte[] body) throws IOException, GeneralSecurityException { - return messageFactory.createAnonymousMessage(parent, - ((PrivateConversationImpl) conversation).getGroup(), - contentType, timestamp, body); + // Validate the arguments + if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH) + throw new IllegalArgumentException(); + if (body.length > MAX_PRIVATE_MESSAGE_BODY_LENGTH) + throw new IllegalArgumentException(); + // Serialise the message + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + w.writeListStart(); + if (parent == null) w.writeNull(); + else w.writeRaw(parent.getBytes()); + w.writeString(contentType); + w.writeRaw(body); + w.writeListEnd(); + Message m = messageFactory.createMessage(groupId, timestamp, + out.toByteArray()); + return new PrivateMessage(m, parent, contentType); } } diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageHeaderImpl.java b/briar-core/src/org/briarproject/messaging/PrivateMessageHeaderImpl.java deleted file mode 100644 index 377447ac81393fee8c112d2563565ec796277832..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/messaging/PrivateMessageHeaderImpl.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.briarproject.messaging; - -import org.briarproject.api.identity.Author; -import org.briarproject.api.messaging.PrivateMessageHeader; -import org.briarproject.api.sync.MessageHeader; -import org.briarproject.api.sync.MessageId; - -// Temporary facade during sync protocol refactoring -public class PrivateMessageHeaderImpl implements PrivateMessageHeader { - - private final MessageHeader messageHeader; - - PrivateMessageHeaderImpl(MessageHeader messageHeader) { - this.messageHeader = messageHeader; - } - - @Override - public MessageId getId() { - return messageHeader.getId(); - } - - @Override - public Author getAuthor() { - return messageHeader.getAuthor(); - } - - @Override - public String getContentType() { - return messageHeader.getContentType(); - } - - @Override - public long getTimestamp() { - return messageHeader.getTimestamp(); - } - - @Override - public boolean isLocal() { - return messageHeader.isLocal(); - } - - @Override - public boolean isRead() { - return messageHeader.isRead(); - } - - @Override - public Status getStatus() { - switch (messageHeader.getStatus()) { - case STORED: - return Status.STORED; - case SENT: - return Status.SENT; - case DELIVERED: - return Status.DELIVERED; - default: - throw new IllegalStateException(); - } - } -} diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..a3b4919380799809513fa8ee5b6e0d4572facbbd --- /dev/null +++ b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java @@ -0,0 +1,103 @@ +package org.briarproject.messaging; + +import org.briarproject.api.FormatException; +import org.briarproject.api.UniqueId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfReader; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.db.Metadata; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageValidator; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.api.system.Clock; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH; +import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; +import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; +import static org.briarproject.messaging.MessagingManagerImpl.CLIENT_ID; + +class PrivateMessageValidator implements MessageValidator { + + private static final Logger LOG = + Logger.getLogger(PrivateMessageValidator.class.getName()); + + private final ValidationManager validationManager; + private final BdfReaderFactory bdfReaderFactory; + private final MetadataEncoder metadataEncoder; + private final Clock clock; + + @Inject + PrivateMessageValidator(ValidationManager validationManager, + BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder, + Clock clock) { + this.validationManager = validationManager; + this.bdfReaderFactory = bdfReaderFactory; + this.metadataEncoder = metadataEncoder; + this.clock = clock; + } + + @Override + public boolean start() { + validationManager.setMessageValidator(CLIENT_ID, this); + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public Metadata validateMessage(Message m) { + // Reject the message if it's too far in the future + long now = clock.currentTimeMillis(); + if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { + LOG.info("Timestamp is too far in the future"); + return null; + } + try { + // Parse the message body + byte[] raw = m.getRaw(); + ByteArrayInputStream in = new ByteArrayInputStream(raw, + MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH); + BdfReader r = bdfReaderFactory.createReader(in); + MessageId parent = null; + String contentType; + r.readListStart(); + // Read the parent ID, if any + if (r.hasRaw()) { + byte[] id = r.readRaw(UniqueId.LENGTH); + if (id.length < UniqueId.LENGTH) throw new FormatException(); + parent = new MessageId(id); + } else { + r.readNull(); + } + // Read the content type + contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); + // Read the private message body + r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH); + r.readListEnd(); + if (!r.eof()) throw new FormatException(); + // Return the metadata + BdfDictionary d = new BdfDictionary(); + d.put("timestamp", m.getTimestamp()); + if (parent != null) d.put("parent", parent.getBytes()); + d.put("contentType", contentType); + d.put("local", false); + d.put("read", false); + return metadataEncoder.encode(d); + } catch (IOException e) { + LOG.info("Invalid private message"); + return null; + } + } +} diff --git a/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java b/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java index 76bf5e8c48552f8ed803f081135df2862334b7d5..eeda6bbd17aeb43623e27a55b412c466d1edd012 100644 --- a/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java +++ b/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java @@ -8,8 +8,8 @@ import org.briarproject.api.plugins.ConnectionRegistry; import org.briarproject.api.plugins.TransportConnectionReader; import org.briarproject.api.plugins.TransportConnectionWriter; import org.briarproject.api.plugins.duplex.DuplexTransportConnection; -import org.briarproject.api.sync.MessagingSession; -import org.briarproject.api.sync.MessagingSessionFactory; +import org.briarproject.api.sync.SyncSession; +import org.briarproject.api.sync.SyncSessionFactory; import org.briarproject.api.transport.KeyManager; import org.briarproject.api.transport.StreamContext; import org.briarproject.api.transport.StreamReaderFactory; @@ -36,20 +36,20 @@ class ConnectionManagerImpl implements ConnectionManager { private final KeyManager keyManager; private final StreamReaderFactory streamReaderFactory; private final StreamWriterFactory streamWriterFactory; - private final MessagingSessionFactory messagingSessionFactory; + private final SyncSessionFactory syncSessionFactory; private final ConnectionRegistry connectionRegistry; @Inject ConnectionManagerImpl(@IoExecutor Executor ioExecutor, KeyManager keyManager, StreamReaderFactory streamReaderFactory, StreamWriterFactory streamWriterFactory, - MessagingSessionFactory messagingSessionFactory, + SyncSessionFactory syncSessionFactory, ConnectionRegistry connectionRegistry) { this.ioExecutor = ioExecutor; this.keyManager = keyManager; this.streamReaderFactory = streamReaderFactory; this.streamWriterFactory = streamWriterFactory; - this.messagingSessionFactory = messagingSessionFactory; + this.syncSessionFactory = syncSessionFactory; this.connectionRegistry = connectionRegistry; } @@ -87,28 +87,28 @@ class ConnectionManagerImpl implements ConnectionManager { return tag; } - private MessagingSession createIncomingSession(StreamContext ctx, + private SyncSession createIncomingSession(StreamContext ctx, TransportConnectionReader r) throws IOException { InputStream streamReader = streamReaderFactory.createStreamReader( r.getInputStream(), ctx); - return messagingSessionFactory.createIncomingSession( + return syncSessionFactory.createIncomingSession( ctx.getContactId(), ctx.getTransportId(), streamReader); } - private MessagingSession createSimplexOutgoingSession(StreamContext ctx, + private SyncSession createSimplexOutgoingSession(StreamContext ctx, TransportConnectionWriter w) throws IOException { OutputStream streamWriter = streamWriterFactory.createStreamWriter( w.getOutputStream(), ctx); - return messagingSessionFactory.createSimplexOutgoingSession( + return syncSessionFactory.createSimplexOutgoingSession( ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(), streamWriter); } - private MessagingSession createDuplexOutgoingSession(StreamContext ctx, + private SyncSession createDuplexOutgoingSession(StreamContext ctx, TransportConnectionWriter w) throws IOException { OutputStream streamWriter = streamWriterFactory.createStreamWriter( w.getOutputStream(), ctx); - return messagingSessionFactory.createDuplexOutgoingSession( + return syncSessionFactory.createDuplexOutgoingSession( ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(), w.getMaxIdleTime(), streamWriter); } @@ -214,8 +214,8 @@ class ConnectionManagerImpl implements ConnectionManager { private final TransportConnectionWriter writer; private volatile ContactId contactId = null; - private volatile MessagingSession incomingSession = null; - private volatile MessagingSession outgoingSession = null; + private volatile SyncSession incomingSession = null; + private volatile SyncSession outgoingSession = null; private ManageIncomingDuplexConnection(TransportId transportId, DuplexTransportConnection transport) { @@ -309,8 +309,8 @@ class ConnectionManagerImpl implements ConnectionManager { private final TransportConnectionReader reader; private final TransportConnectionWriter writer; - private volatile MessagingSession incomingSession = null; - private volatile MessagingSession outgoingSession = null; + private volatile SyncSession incomingSession = null; + private volatile SyncSession outgoingSession = null; private ManageOutgoingDuplexConnection(ContactId contactId, TransportId transportId, DuplexTransportConnection transport) { diff --git a/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java b/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java index aad2aceca273a90560631dc89e48b2c5c3140771..bbe2674d0a9632ccbdeba9a3e6c8c36912856c57 100644 --- a/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java +++ b/briar-core/src/org/briarproject/sync/AuthorFactoryImpl.java @@ -1,7 +1,6 @@ package org.briarproject.sync; import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.MessageDigest; import org.briarproject.api.data.BdfWriter; import org.briarproject.api.data.BdfWriterFactory; import org.briarproject.api.identity.Author; @@ -49,10 +48,8 @@ class AuthorFactoryImpl implements AuthorFactory { w.writeListEnd(); } catch (IOException e) { // Shouldn't happen with ByteArrayOutputStream - throw new RuntimeException(); + throw new RuntimeException(e); } - MessageDigest messageDigest = crypto.getMessageDigest(); - messageDigest.update(out.toByteArray()); - return new AuthorId(messageDigest.digest()); + return new AuthorId(crypto.hash(AuthorId.LABEL, out.toByteArray())); } } diff --git a/briar-core/src/org/briarproject/sync/AuthorReader.java b/briar-core/src/org/briarproject/sync/AuthorReader.java index 5715bdf9c7f197db8ccae0e563af6f396fb7ea9b..8a4b85300200ef9993d3a8b04aa6cb2f0213f497 100644 --- a/briar-core/src/org/briarproject/sync/AuthorReader.java +++ b/briar-core/src/org/briarproject/sync/AuthorReader.java @@ -1,12 +1,10 @@ package org.briarproject.sync; import org.briarproject.api.FormatException; -import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.MessageDigest; import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.ObjectReader; import org.briarproject.api.identity.Author; -import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.AuthorFactory; import java.io.IOException; @@ -15,26 +13,18 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGT class AuthorReader implements ObjectReader<Author> { - private final MessageDigest messageDigest; + private final AuthorFactory authorFactory; - AuthorReader(CryptoComponent crypto) { - messageDigest = crypto.getMessageDigest(); + AuthorReader(AuthorFactory authorFactory) { + this.authorFactory = authorFactory; } public Author readObject(BdfReader r) throws IOException { - // Set up the reader - DigestingConsumer digesting = new DigestingConsumer(messageDigest); - r.addConsumer(digesting); - // Read and digest the data r.readListStart(); String name = r.readString(MAX_AUTHOR_NAME_LENGTH); if (name.length() == 0) throw new FormatException(); byte[] publicKey = r.readRaw(MAX_PUBLIC_KEY_LENGTH); r.readListEnd(); - // Reset the reader - r.removeConsumer(digesting); - // Build and return the author - AuthorId id = new AuthorId(messageDigest.digest()); - return new Author(id, name, publicKey); + return authorFactory.createAuthor(name, publicKey); } } diff --git a/briar-core/src/org/briarproject/sync/CopyingConsumer.java b/briar-core/src/org/briarproject/sync/CopyingConsumer.java deleted file mode 100644 index 39ee00b25fe4a10962849c175ed1941fa62c1d8d..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/sync/CopyingConsumer.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.api.data.Consumer; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -/** A consumer that makes a copy of the bytes consumed. */ -class CopyingConsumer implements Consumer { - - private final ByteArrayOutputStream out = new ByteArrayOutputStream(); - - public byte[] getCopy() { - return out.toByteArray(); - } - - public void write(byte b) throws IOException { - out.write(b); - } - - public void write(byte[] b, int off, int len) throws IOException { - out.write(b, off, len); - } -} diff --git a/briar-core/src/org/briarproject/sync/CountingConsumer.java b/briar-core/src/org/briarproject/sync/CountingConsumer.java deleted file mode 100644 index 63b2874e07e40ff3e98218706ab5f32c46951ef9..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/sync/CountingConsumer.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.api.FormatException; -import org.briarproject.api.data.Consumer; - -import java.io.IOException; - -/** - * A consumer that counts the number of bytes consumed and throws a - * FormatException if the count exceeds a given limit. - */ -class CountingConsumer implements Consumer { - - private final long limit; - private long count = 0; - - public CountingConsumer(long limit) { - this.limit = limit; - } - - public long getCount() { - return count; - } - - public void write(byte b) throws IOException { - count++; - if (count > limit) throw new FormatException(); - } - - public void write(byte[] b, int off, int len) throws IOException { - count += len; - if (count > limit) throw new FormatException(); - } -} diff --git a/briar-core/src/org/briarproject/sync/DigestingConsumer.java b/briar-core/src/org/briarproject/sync/DigestingConsumer.java deleted file mode 100644 index ee217334e878dda00880a4421ce581668b710a1c..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/sync/DigestingConsumer.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.api.crypto.MessageDigest; -import org.briarproject.api.data.Consumer; - -/** A consumer that passes its input through a message digest. */ -class DigestingConsumer implements Consumer { - - private final MessageDigest messageDigest; - - public DigestingConsumer(MessageDigest messageDigest) { - this.messageDigest = messageDigest; - } - - public void write(byte b) { - messageDigest.update(b); - } - - public void write(byte[] b, int off, int len) { - messageDigest.update(b, off, len); - } -} diff --git a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java index 0a8751b2b7111439e0ff44efe12c67588df55499..1d14d737875f9da93fe8ea72284270addb65b994 100644 --- a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java +++ b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java @@ -10,21 +10,21 @@ import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventListener; import org.briarproject.api.event.LocalSubscriptionsUpdatedEvent; import org.briarproject.api.event.LocalTransportsUpdatedEvent; -import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.RemoteSubscriptionsUpdatedEvent; import org.briarproject.api.event.RemoteTransportsUpdatedEvent; import org.briarproject.api.event.ShutdownEvent; import org.briarproject.api.event.TransportRemovedEvent; import org.briarproject.api.sync.Ack; -import org.briarproject.api.sync.MessagingSession; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.PacketWriter; import org.briarproject.api.sync.Request; import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionUpdate; +import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportUpdate; import org.briarproject.api.system.Clock; @@ -39,15 +39,15 @@ import java.util.logging.Logger; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH; /** - * An outgoing {@link org.briarproject.api.sync.MessagingSession - * MessagingSession} suitable for duplex transports. The session offers - * messages before sending them, keeps its output stream open when there are no - * packets to send, and reacts to events that make packets available to send. + * An outgoing {@link org.briarproject.api.sync.SyncSession SyncSession} + * suitable for duplex transports. The session offers messages before sending + * them, keeps its output stream open when there are no packets to send, and + * reacts to events that make packets available to send. */ -class DuplexOutgoingSession implements MessagingSession, EventListener { +class DuplexOutgoingSession implements SyncSession, EventListener { // Check for retransmittable packets once every 60 seconds private static final int RETX_QUERY_INTERVAL = 60 * 1000; @@ -161,8 +161,9 @@ class DuplexOutgoingSession implements MessagingSession, EventListener { if (e instanceof ContactRemovedEvent) { ContactRemovedEvent c = (ContactRemovedEvent) e; if (c.getContactId().equals(contactId)) interrupt(); - } else if (e instanceof MessageAddedEvent) { - dbExecutor.execute(new GenerateOffer()); + } else if (e instanceof MessageValidatedEvent) { + if (((MessageValidatedEvent) e).isValid()) + dbExecutor.execute(new GenerateOffer()); } else if (e instanceof LocalSubscriptionsUpdatedEvent) { LocalSubscriptionsUpdatedEvent l = (LocalSubscriptionsUpdatedEvent) e; @@ -243,7 +244,7 @@ class DuplexOutgoingSession implements MessagingSession, EventListener { if (interrupted) return; try { Collection<byte[]> b = db.generateRequestedBatch(contactId, - MAX_PAYLOAD_LENGTH, maxLatency); + MAX_PACKET_PAYLOAD_LENGTH, maxLatency); if (LOG.isLoggable(INFO)) LOG.info("Generated batch: " + (b != null)); if (b != null) writerTasks.add(new WriteBatch(b)); diff --git a/briar-core/src/org/briarproject/sync/GroupFactoryImpl.java b/briar-core/src/org/briarproject/sync/GroupFactoryImpl.java index 6f36b0f30201eefca0ced9033ad4d706f52ee0f6..9c00b15cb6bdfe15fc8d17e66b280e2f92b0a67f 100644 --- a/briar-core/src/org/briarproject/sync/GroupFactoryImpl.java +++ b/briar-core/src/org/briarproject/sync/GroupFactoryImpl.java @@ -1,52 +1,24 @@ package org.briarproject.sync; import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.MessageDigest; -import org.briarproject.api.data.BdfWriter; -import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.GroupId; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - import javax.inject.Inject; -import static org.briarproject.api.sync.MessagingConstants.GROUP_SALT_LENGTH; - class GroupFactoryImpl implements GroupFactory { private final CryptoComponent crypto; - private final BdfWriterFactory bdfWriterFactory; @Inject - GroupFactoryImpl(CryptoComponent crypto, BdfWriterFactory bdfWriterFactory) { + GroupFactoryImpl(CryptoComponent crypto) { this.crypto = crypto; - this.bdfWriterFactory = bdfWriterFactory; - } - - public Group createGroup(String name) { - byte[] salt = new byte[GROUP_SALT_LENGTH]; - crypto.getSecureRandom().nextBytes(salt); - return createGroup(name, salt); } - public Group createGroup(String name, byte[] salt) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - BdfWriter w = bdfWriterFactory.createWriter(out); - try { - w.writeListStart(); - w.writeString(name); - w.writeRaw(salt); - w.writeListEnd(); - } catch (IOException e) { - // Shouldn't happen with ByteArrayOutputStream - throw new RuntimeException(); - } - MessageDigest messageDigest = crypto.getMessageDigest(); - messageDigest.update(out.toByteArray()); - GroupId id = new GroupId(messageDigest.digest()); - return new Group(id, name, salt); + public Group createGroup(ClientId c, byte[] descriptor) { + byte[] hash = crypto.hash(GroupId.LABEL, c.getBytes(), descriptor); + return new Group(new GroupId(hash), c, descriptor); } } diff --git a/briar-core/src/org/briarproject/sync/GroupReader.java b/briar-core/src/org/briarproject/sync/GroupReader.java index 529a8170a7cdd01be8228e5cbd5ed358bbdd44c2..2500c8b1c968ed69d628fcdc0d4f6575fe90b4ec 100644 --- a/briar-core/src/org/briarproject/sync/GroupReader.java +++ b/briar-core/src/org/briarproject/sync/GroupReader.java @@ -1,39 +1,31 @@ package org.briarproject.sync; import org.briarproject.api.FormatException; -import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.MessageDigest; +import org.briarproject.api.UniqueId; import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.ObjectReader; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.GroupFactory; import java.io.IOException; -import static org.briarproject.api.sync.MessagingConstants.GROUP_SALT_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_GROUP_NAME_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; class GroupReader implements ObjectReader<Group> { - private final MessageDigest messageDigest; + private final GroupFactory groupFactory; - GroupReader(CryptoComponent crypto) { - messageDigest = crypto.getMessageDigest(); + GroupReader(GroupFactory groupFactory) { + this.groupFactory = groupFactory; } public Group readObject(BdfReader r) throws IOException { - DigestingConsumer digesting = new DigestingConsumer(messageDigest); - // Read and digest the data - r.addConsumer(digesting); r.readListStart(); - String name = r.readString(MAX_GROUP_NAME_LENGTH); - if (name.length() == 0) throw new FormatException(); - byte[] salt = r.readRaw(GROUP_SALT_LENGTH); - if (salt.length != GROUP_SALT_LENGTH) throw new FormatException(); + byte[] id = r.readRaw(UniqueId.LENGTH); + if (id.length != UniqueId.LENGTH) throw new FormatException(); + byte[] descriptor = r.readRaw(MAX_GROUP_DESCRIPTOR_LENGTH); r.readListEnd(); - r.removeConsumer(digesting); - // Build and return the group - GroupId id = new GroupId(messageDigest.digest()); - return new Group(id, name, salt); + return groupFactory.createGroup(new ClientId(id), descriptor); } } diff --git a/briar-core/src/org/briarproject/sync/IncomingSession.java b/briar-core/src/org/briarproject/sync/IncomingSession.java index 907ffaf195ddad2265c346fac4e972d56c5b953a..9e8bd2caa95e385b2c761858947b8d168e7cedcd 100644 --- a/briar-core/src/org/briarproject/sync/IncomingSession.java +++ b/briar-core/src/org/briarproject/sync/IncomingSession.java @@ -13,37 +13,30 @@ import org.briarproject.api.event.ShutdownEvent; import org.briarproject.api.event.TransportRemovedEvent; import org.briarproject.api.sync.Ack; import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageVerifier; -import org.briarproject.api.sync.MessagingSession; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.PacketReader; import org.briarproject.api.sync.Request; import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionUpdate; +import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportUpdate; -import org.briarproject.api.sync.UnverifiedMessage; import java.io.IOException; -import java.security.GeneralSecurityException; import java.util.concurrent.Executor; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; -/** - * An incoming {@link org.briarproject.api.sync.MessagingSession - * MessagingSession}. - */ -class IncomingSession implements MessagingSession, EventListener { +/** An incoming {@link org.briarproject.api.sync.SyncSession SyncSession}. */ +class IncomingSession implements SyncSession, EventListener { private static final Logger LOG = Logger.getLogger(IncomingSession.class.getName()); private final DatabaseComponent db; - private final Executor dbExecutor, cryptoExecutor; + private final Executor dbExecutor; private final EventBus eventBus; - private final MessageVerifier messageVerifier; private final ContactId contactId; private final TransportId transportId; private final PacketReader packetReader; @@ -51,14 +44,11 @@ class IncomingSession implements MessagingSession, EventListener { private volatile boolean interrupted = false; IncomingSession(DatabaseComponent db, Executor dbExecutor, - Executor cryptoExecutor, EventBus eventBus, - MessageVerifier messageVerifier, ContactId contactId, - TransportId transportId, PacketReader packetReader) { + EventBus eventBus, ContactId contactId, TransportId transportId, + PacketReader packetReader) { this.db = db; this.dbExecutor = dbExecutor; - this.cryptoExecutor = cryptoExecutor; this.eventBus = eventBus; - this.messageVerifier = messageVerifier; this.contactId = contactId; this.transportId = transportId; this.packetReader = packetReader; @@ -73,8 +63,8 @@ class IncomingSession implements MessagingSession, EventListener { Ack a = packetReader.readAck(); dbExecutor.execute(new ReceiveAck(a)); } else if (packetReader.hasMessage()) { - UnverifiedMessage m = packetReader.readMessage(); - cryptoExecutor.execute(new VerifyMessage(m)); + Message m = packetReader.readMessage(); + dbExecutor.execute(new ReceiveMessage(m)); } else if (packetReader.hasOffer()) { Offer o = packetReader.readOffer(); dbExecutor.execute(new ReceiveOffer(o)); @@ -137,25 +127,6 @@ class IncomingSession implements MessagingSession, EventListener { } } - private class VerifyMessage implements Runnable { - - private final UnverifiedMessage message; - - private VerifyMessage(UnverifiedMessage message) { - this.message = message; - } - - public void run() { - try { - Message m = messageVerifier.verifyMessage(message); - dbExecutor.execute(new ReceiveMessage(m)); - } catch (GeneralSecurityException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - interrupt(); - } - } - } - private class ReceiveMessage implements Runnable { private final Message message; diff --git a/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java b/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java index 493713e3bd8366bda1547837cb3d6d053b8e4b67..e32ec110f31602461b2dc1de352b8660139e18c5 100644 --- a/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java +++ b/briar-core/src/org/briarproject/sync/MessageFactoryImpl.java @@ -1,129 +1,39 @@ package org.briarproject.sync; +import com.google.inject.Inject; + +import org.briarproject.api.UniqueId; import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.MessageDigest; -import org.briarproject.api.crypto.PrivateKey; -import org.briarproject.api.crypto.Signature; -import org.briarproject.api.data.BdfWriter; -import org.briarproject.api.data.BdfWriterFactory; -import org.briarproject.api.data.Consumer; -import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageFactory; import org.briarproject.api.sync.MessageId; -import org.briarproject.util.StringUtils; +import org.briarproject.util.ByteUtils; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.SecureRandom; - -import javax.inject.Inject; -import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_BODY_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_CONTENT_TYPE_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MESSAGE_SALT_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; class MessageFactoryImpl implements MessageFactory { - private final Signature signature; - private final SecureRandom random; - private final MessageDigest messageDigest; - private final BdfWriterFactory bdfWriterFactory; + private final CryptoComponent crypto; @Inject - MessageFactoryImpl(CryptoComponent crypto, BdfWriterFactory bdfWriterFactory) { - signature = crypto.getSignature(); - random = crypto.getSecureRandom(); - messageDigest = crypto.getMessageDigest(); - this.bdfWriterFactory = bdfWriterFactory; - } - - public Message createAnonymousMessage(MessageId parent, Group group, - String contentType, long timestamp, byte[] body) throws IOException, - GeneralSecurityException { - return createMessage(parent, group, null, null, contentType, timestamp, - body); - } - - public Message createPseudonymousMessage(MessageId parent, Group group, - Author author, PrivateKey privateKey, String contentType, - long timestamp, byte[] body) throws IOException, - GeneralSecurityException { - return createMessage(parent, group, author, privateKey, contentType, - timestamp, body); + MessageFactoryImpl(CryptoComponent crypto) { + this.crypto = crypto; } - private Message createMessage(MessageId parent, Group group, Author author, - PrivateKey privateKey, String contentType, long timestamp, - byte[] body) throws IOException, GeneralSecurityException { - // Validate the arguments - if ((author == null) != (privateKey == null)) + @Override + public Message createMessage(GroupId groupId, long timestamp, byte[] body) + throws IOException { + if (body.length > MAX_MESSAGE_BODY_LENGTH) throw new IllegalArgumentException(); - if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH) - throw new IllegalArgumentException(); - if (body.length > MAX_BODY_LENGTH) - throw new IllegalArgumentException(); - // Serialise the message to a buffer - ByteArrayOutputStream out = new ByteArrayOutputStream(); - BdfWriter w = bdfWriterFactory.createWriter(out); - // Initialise the consumers - CountingConsumer counting = new CountingConsumer(MAX_PAYLOAD_LENGTH); - w.addConsumer(counting); - Consumer digestingConsumer = new DigestingConsumer(messageDigest); - w.addConsumer(digestingConsumer); - Consumer signingConsumer = null; - if (privateKey != null) { - signature.initSign(privateKey); - signingConsumer = new SigningConsumer(signature); - w.addConsumer(signingConsumer); - } - // Write the message - w.writeListStart(); - if (parent == null) w.writeNull(); - else w.writeRaw(parent.getBytes()); - writeGroup(w, group); - if (author == null) w.writeNull(); - else writeAuthor(w, author); - w.writeString(contentType); - w.writeInteger(timestamp); - byte[] salt = new byte[MESSAGE_SALT_LENGTH]; - random.nextBytes(salt); - w.writeRaw(salt); - w.writeRaw(body); - int bodyStart = (int) counting.getCount() - body.length; - // Sign the message with the author's private key, if there is one - if (privateKey == null) { - w.writeNull(); - } else { - w.removeConsumer(signingConsumer); - byte[] sig = signature.sign(); - if (sig.length > MAX_SIGNATURE_LENGTH) - throw new IllegalArgumentException(); - w.writeRaw(sig); - } - w.writeListEnd(); - // Hash the message, including the signature, to get the message ID - w.removeConsumer(digestingConsumer); - MessageId id = new MessageId(messageDigest.digest()); - return new MessageImpl(id, parent, group, author, contentType, - timestamp, out.toByteArray(), bodyStart, body.length); - } - - private void writeGroup(BdfWriter w, Group g) throws IOException { - w.writeListStart(); - w.writeString(g.getName()); - w.writeRaw(g.getSalt()); - w.writeListEnd(); - } - - private void writeAuthor(BdfWriter w, Author a) throws IOException { - w.writeListStart(); - w.writeString(a.getName()); - w.writeRaw(a.getPublicKey()); - w.writeListEnd(); + byte[] raw = new byte[MESSAGE_HEADER_LENGTH + body.length]; + System.arraycopy(groupId.getBytes(), 0, raw, 0, UniqueId.LENGTH); + ByteUtils.writeUint64(timestamp, raw, UniqueId.LENGTH); + System.arraycopy(body, 0, raw, MESSAGE_HEADER_LENGTH, body.length); + MessageId id = new MessageId(crypto.hash(MessageId.LABEL, raw)); + return new Message(id, groupId, timestamp, raw); } } diff --git a/briar-core/src/org/briarproject/sync/MessageImpl.java b/briar-core/src/org/briarproject/sync/MessageImpl.java deleted file mode 100644 index dc2f7af170e07f710739831bbe21ae3565b874d0..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/sync/MessageImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageId; - -import static org.briarproject.api.sync.MessagingConstants.MAX_BODY_LENGTH; - -/** A simple in-memory implementation of a message. */ -class MessageImpl implements Message { - - private final MessageId id, parent; - private final Group group; - private final Author author; - private final String contentType; - private final long timestamp; - private final byte[] raw; - private final int bodyStart, bodyLength; - - public MessageImpl(MessageId id, MessageId parent, Group group, - Author author, String contentType, long timestamp, - byte[] raw, int bodyStart, int bodyLength) { - if (bodyStart + bodyLength > raw.length) - throw new IllegalArgumentException(); - if (bodyLength > MAX_BODY_LENGTH) - throw new IllegalArgumentException(); - this.id = id; - this.parent = parent; - this.group = group; - this.author = author; - this.contentType = contentType; - this.timestamp = timestamp; - this.raw = raw; - this.bodyStart = bodyStart; - this.bodyLength = bodyLength; - } - - public MessageId getId() { - return id; - } - - public MessageId getParent() { - return parent; - } - - public Group getGroup() { - return group; - } - - public Author getAuthor() { - return author; - } - - public String getContentType() { - return contentType; - } - - public long getTimestamp() { - return timestamp; - } - - public byte[] getSerialised() { - return raw; - } - - public int getBodyStart() { - return bodyStart; - } - - public int getBodyLength() { - return bodyLength; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object o) { - return o instanceof Message && id.equals(((Message) o).getId()); - } -} diff --git a/briar-core/src/org/briarproject/sync/MessageReader.java b/briar-core/src/org/briarproject/sync/MessageReader.java deleted file mode 100644 index 617d9fa68621ea07e90da88ea01b5e4fd2bd1979..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/sync/MessageReader.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.api.FormatException; -import org.briarproject.api.UniqueId; -import org.briarproject.api.data.BdfReader; -import org.briarproject.api.data.ObjectReader; -import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.MessageId; -import org.briarproject.api.sync.UnverifiedMessage; - -import java.io.IOException; - -import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_BODY_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_CONTENT_TYPE_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MESSAGE_SALT_LENGTH; - -class MessageReader implements ObjectReader<UnverifiedMessage> { - - private final ObjectReader<Group> groupReader; - private final ObjectReader<Author> authorReader; - - MessageReader(ObjectReader<Group> groupReader, - ObjectReader<Author> authorReader) { - this.groupReader = groupReader; - this.authorReader = authorReader; - } - - public UnverifiedMessage readObject(BdfReader r) throws IOException { - CopyingConsumer copying = new CopyingConsumer(); - CountingConsumer counting = new CountingConsumer(MAX_PAYLOAD_LENGTH); - r.addConsumer(copying); - r.addConsumer(counting); - // Read the start of the message - r.readListStart(); - // Read the parent's message ID, if there is one - MessageId parent = null; - if (r.hasNull()) { - r.readNull(); - } else { - byte[] b = r.readRaw(UniqueId.LENGTH); - if (b.length < UniqueId.LENGTH) throw new FormatException(); - parent = new MessageId(b); - } - // Read the group - Group group = groupReader.readObject(r); - // Read the author, if there is one - Author author = null; - if (r.hasNull()) r.readNull(); - else author = authorReader.readObject(r); - // Read the content type - String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH); - // Read the timestamp - long timestamp = r.readInteger(); - if (timestamp < 0) throw new FormatException(); - // Read the salt - byte[] salt = r.readRaw(MESSAGE_SALT_LENGTH); - if (salt.length < MESSAGE_SALT_LENGTH) throw new FormatException(); - // Read the message body - byte[] body = r.readRaw(MAX_BODY_LENGTH); - // Record the offset of the body within the message - int bodyStart = (int) counting.getCount() - body.length; - // Record the length of the data covered by the author's signature - int signedLength = (int) counting.getCount(); - // Read the author's signature, if there is one - byte[] signature = null; - if (author == null) r.readNull(); - else signature = r.readRaw(MAX_SIGNATURE_LENGTH); - // Read the end of the message - r.readListEnd(); - // Reset the reader - r.removeConsumer(counting); - r.removeConsumer(copying); - // Build and return the unverified message - byte[] raw = copying.getCopy(); - return new UnverifiedMessage(parent, group, author, contentType, - timestamp, raw, signature, bodyStart, body.length, - signedLength); - } -} diff --git a/briar-core/src/org/briarproject/sync/MessageVerifierImpl.java b/briar-core/src/org/briarproject/sync/MessageVerifierImpl.java deleted file mode 100644 index 13fc03abb5aca1c7f4c1a2ebc227706229132a16..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/sync/MessageVerifierImpl.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.KeyParser; -import org.briarproject.api.crypto.MessageDigest; -import org.briarproject.api.crypto.PublicKey; -import org.briarproject.api.crypto.Signature; -import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageId; -import org.briarproject.api.sync.MessageVerifier; -import org.briarproject.api.sync.UnverifiedMessage; -import org.briarproject.api.system.Clock; - -import java.security.GeneralSecurityException; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import static java.util.logging.Level.INFO; -import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; - -class MessageVerifierImpl implements MessageVerifier { - - private static final Logger LOG = - Logger.getLogger(MessageVerifierImpl.class.getName()); - - private final CryptoComponent crypto; - private final Clock clock; - private final KeyParser keyParser; - - @Inject - MessageVerifierImpl(CryptoComponent crypto, Clock clock) { - this.crypto = crypto; - this.clock = clock; - keyParser = crypto.getSignatureKeyParser(); - } - - public Message verifyMessage(UnverifiedMessage m) - throws GeneralSecurityException { - long now = System.currentTimeMillis(); - MessageDigest messageDigest = crypto.getMessageDigest(); - Signature signature = crypto.getSignature(); - // Reject the message if it's too far in the future - if (m.getTimestamp() > clock.currentTimeMillis() + MAX_CLOCK_DIFFERENCE) - throw new GeneralSecurityException(); - // Hash the message to get the message ID - byte[] raw = m.getSerialised(); - messageDigest.update(raw); - MessageId id = new MessageId(messageDigest.digest()); - // Verify the author's signature, if there is one - Author author = m.getAuthor(); - if (author != null) { - PublicKey k = keyParser.parsePublicKey(author.getPublicKey()); - signature.initVerify(k); - signature.update(raw, 0, m.getSignedLength()); - if (!signature.verify(m.getSignature())) - throw new GeneralSecurityException(); - } - Message verified = new MessageImpl(id, m.getParent(), m.getGroup(), - author, m.getContentType(), m.getTimestamp(), raw, - m.getBodyStart(), m.getBodyLength()); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Verifying message took " + duration + " ms"); - return verified; - } -} diff --git a/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java b/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java index ed37e602c6fb2fdf3f1c7224507473f03b988510..eac4b931f9906d6e3a34cc105ea16713ac1a1148 100644 --- a/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java +++ b/briar-core/src/org/briarproject/sync/PacketReaderFactoryImpl.java @@ -1,11 +1,11 @@ package org.briarproject.sync; +import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.ObjectReader; import org.briarproject.api.sync.PacketReader; import org.briarproject.api.sync.PacketReaderFactory; import org.briarproject.api.sync.SubscriptionUpdate; -import org.briarproject.api.sync.UnverifiedMessage; import java.io.InputStream; @@ -13,21 +13,21 @@ import javax.inject.Inject; class PacketReaderFactoryImpl implements PacketReaderFactory { + private final CryptoComponent crypto; private final BdfReaderFactory bdfReaderFactory; - private final ObjectReader<UnverifiedMessage> messageReader; private final ObjectReader<SubscriptionUpdate> subscriptionUpdateReader; @Inject - PacketReaderFactoryImpl(BdfReaderFactory bdfReaderFactory, - ObjectReader<UnverifiedMessage> messageReader, + PacketReaderFactoryImpl(CryptoComponent crypto, + BdfReaderFactory bdfReaderFactory, ObjectReader<SubscriptionUpdate> subscriptionUpdateReader) { + this.crypto = crypto; this.bdfReaderFactory = bdfReaderFactory; - this.messageReader = messageReader; this.subscriptionUpdateReader = subscriptionUpdateReader; } public PacketReader createPacketReader(InputStream in) { - return new PacketReaderImpl(bdfReaderFactory, messageReader, + return new PacketReaderImpl(crypto, bdfReaderFactory, subscriptionUpdateReader, in); } } diff --git a/briar-core/src/org/briarproject/sync/PacketReaderImpl.java b/briar-core/src/org/briarproject/sync/PacketReaderImpl.java index 08a6632abe60168656f3178f7776c977ab0b090c..2d1a8bfc7ce4f27ffe76bf69c15c57fc822f99e7 100644 --- a/briar-core/src/org/briarproject/sync/PacketReaderImpl.java +++ b/briar-core/src/org/briarproject/sync/PacketReaderImpl.java @@ -4,10 +4,13 @@ import org.briarproject.api.FormatException; import org.briarproject.api.TransportId; import org.briarproject.api.TransportProperties; import org.briarproject.api.UniqueId; +import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.data.BdfReader; import org.briarproject.api.data.BdfReaderFactory; import org.briarproject.api.data.ObjectReader; import org.briarproject.api.sync.Ack; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.PacketReader; @@ -16,7 +19,6 @@ import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportUpdate; -import org.briarproject.api.sync.UnverifiedMessage; import org.briarproject.util.ByteUtils; import java.io.ByteArrayInputStream; @@ -31,9 +33,6 @@ import java.util.Map; import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT; import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTY_LENGTH; import static org.briarproject.api.TransportPropertyConstants.MAX_TRANSPORT_ID_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.HEADER_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.PROTOCOL_VERSION; import static org.briarproject.api.sync.PacketTypes.ACK; import static org.briarproject.api.sync.PacketTypes.MESSAGE; import static org.briarproject.api.sync.PacketTypes.OFFER; @@ -42,14 +41,18 @@ import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_ACK; import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_UPDATE; import static org.briarproject.api.sync.PacketTypes.TRANSPORT_ACK; import static org.briarproject.api.sync.PacketTypes.TRANSPORT_UPDATE; +import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; +import static org.briarproject.api.sync.SyncConstants.PACKET_HEADER_LENGTH; +import static org.briarproject.api.sync.SyncConstants.PROTOCOL_VERSION; // This class is not thread-safe class PacketReaderImpl implements PacketReader { private enum State { BUFFER_EMPTY, BUFFER_FULL, EOF } + private final CryptoComponent crypto; private final BdfReaderFactory bdfReaderFactory; - private final ObjectReader<UnverifiedMessage> messageReader; private final ObjectReader<SubscriptionUpdate> subscriptionUpdateReader; private final InputStream in; private final byte[] header, payload; @@ -57,24 +60,23 @@ class PacketReaderImpl implements PacketReader { private State state = State.BUFFER_EMPTY; private int payloadLength = 0; - PacketReaderImpl(BdfReaderFactory bdfReaderFactory, - ObjectReader<UnverifiedMessage> messageReader, + PacketReaderImpl(CryptoComponent crypto, BdfReaderFactory bdfReaderFactory, ObjectReader<SubscriptionUpdate> subscriptionUpdateReader, InputStream in) { + this.crypto = crypto; this.bdfReaderFactory = bdfReaderFactory; - this.messageReader = messageReader; this.subscriptionUpdateReader = subscriptionUpdateReader; this.in = in; - header = new byte[HEADER_LENGTH]; - payload = new byte[MAX_PAYLOAD_LENGTH]; + header = new byte[PACKET_HEADER_LENGTH]; + payload = new byte[MAX_PACKET_PAYLOAD_LENGTH]; } private void readPacket() throws IOException { - assert state == State.BUFFER_EMPTY; + if (state != State.BUFFER_EMPTY) throw new IllegalStateException(); // Read the header int offset = 0; - while (offset < HEADER_LENGTH) { - int read = in.read(header, offset, HEADER_LENGTH - offset); + while (offset < PACKET_HEADER_LENGTH) { + int read = in.read(header, offset, PACKET_HEADER_LENGTH - offset); if (read == -1) { if (offset > 0) throw new FormatException(); state = State.EOF; @@ -86,7 +88,7 @@ class PacketReaderImpl implements PacketReader { if (header[0] != PROTOCOL_VERSION) throw new FormatException(); // Read the payload length payloadLength = ByteUtils.readUint16(header, 2); - if (payloadLength > MAX_PAYLOAD_LENGTH) throw new FormatException(); + if (payloadLength > MAX_PACKET_PAYLOAD_LENGTH) throw new FormatException(); // Read the payload offset = 0; while (offset < payloadLength) { @@ -99,7 +101,7 @@ class PacketReaderImpl implements PacketReader { public boolean eof() throws IOException { if (state == State.BUFFER_EMPTY) readPacket(); - assert state != State.BUFFER_EMPTY; + if (state == State.BUFFER_EMPTY) throw new IllegalStateException(); return state == State.EOF; } @@ -109,44 +111,43 @@ class PacketReaderImpl implements PacketReader { public Ack readAck() throws IOException { if (!hasAck()) throw new FormatException(); - // Set up the reader - InputStream bais = new ByteArrayInputStream(payload, 0, payloadLength); - BdfReader r = bdfReaderFactory.createReader(bais); - // Read the start of the payload - r.readListStart(); - // Read the message IDs - List<MessageId> acked = new ArrayList<MessageId>(); - r.readListStart(); - while (!r.hasListEnd()) { - byte[] b = r.readRaw(UniqueId.LENGTH); - if (b.length != UniqueId.LENGTH) - throw new FormatException(); - acked.add(new MessageId(b)); + return new Ack(Collections.unmodifiableList(readMessageIds())); + } + + private List<MessageId> readMessageIds() throws IOException { + if (payloadLength == 0) throw new FormatException(); + if (payloadLength % UniqueId.LENGTH != 0) throw new FormatException(); + List<MessageId> ids = new ArrayList<MessageId>(); + for (int off = 0; off < payloadLength; off += UniqueId.LENGTH) { + byte[] id = new byte[UniqueId.LENGTH]; + System.arraycopy(payload, off, id, 0, UniqueId.LENGTH); + ids.add(new MessageId(id)); } - if (acked.isEmpty()) throw new FormatException(); - r.readListEnd(); - // Read the end of the payload - r.readListEnd(); - if (!r.eof()) throw new FormatException(); state = State.BUFFER_EMPTY; - // Build and return the ack - return new Ack(Collections.unmodifiableList(acked)); + return ids; } public boolean hasMessage() throws IOException { return !eof() && header[1] == MESSAGE; } - public UnverifiedMessage readMessage() throws IOException { + public Message readMessage() throws IOException { if (!hasMessage()) throw new FormatException(); - // Set up the reader - InputStream bais = new ByteArrayInputStream(payload, 0, payloadLength); - BdfReader r = bdfReaderFactory.createReader(bais); - // Read and build the message - UnverifiedMessage m = messageReader.readObject(r); - if (!r.eof()) throw new FormatException(); + if (payloadLength <= MESSAGE_HEADER_LENGTH) throw new FormatException(); + // Group ID + byte[] id = new byte[UniqueId.LENGTH]; + System.arraycopy(payload, 0, id, 0, UniqueId.LENGTH); + GroupId groupId = new GroupId(id); + // Timestamp + long timestamp = ByteUtils.readUint64(payload, UniqueId.LENGTH); + if (timestamp < 0) throw new FormatException(); + // Raw message + byte[] raw = new byte[payloadLength]; + System.arraycopy(payload, 0, raw, 0, payloadLength); state = State.BUFFER_EMPTY; - return m; + // Message ID + MessageId messageId = new MessageId(crypto.hash(MessageId.LABEL, raw)); + return new Message(messageId, groupId, timestamp, raw); } public boolean hasOffer() throws IOException { @@ -155,28 +156,7 @@ class PacketReaderImpl implements PacketReader { public Offer readOffer() throws IOException { if (!hasOffer()) throw new FormatException(); - // Set up the reader - InputStream bais = new ByteArrayInputStream(payload, 0, payloadLength); - BdfReader r = bdfReaderFactory.createReader(bais); - // Read the start of the payload - r.readListStart(); - // Read the message IDs - List<MessageId> offered = new ArrayList<MessageId>(); - r.readListStart(); - while (!r.hasListEnd()) { - byte[] b = r.readRaw(UniqueId.LENGTH); - if (b.length != UniqueId.LENGTH) - throw new FormatException(); - offered.add(new MessageId(b)); - } - if (offered.isEmpty()) throw new FormatException(); - r.readListEnd(); - // Read the end of the payload - r.readListEnd(); - if (!r.eof()) throw new FormatException(); - state = State.BUFFER_EMPTY; - // Build and return the offer - return new Offer(Collections.unmodifiableList(offered)); + return new Offer(Collections.unmodifiableList(readMessageIds())); } public boolean hasRequest() throws IOException { @@ -185,28 +165,7 @@ class PacketReaderImpl implements PacketReader { public Request readRequest() throws IOException { if (!hasRequest()) throw new FormatException(); - // Set up the reader - InputStream bais = new ByteArrayInputStream(payload, 0, payloadLength); - BdfReader r = bdfReaderFactory.createReader(bais); - // Read the start of the payload - r.readListStart(); - // Read the message IDs - r.readListStart(); - List<MessageId> requested = new ArrayList<MessageId>(); - while (!r.hasListEnd()) { - byte[] b = r.readRaw(UniqueId.LENGTH); - if (b.length != UniqueId.LENGTH) - throw new FormatException(); - requested.add(new MessageId(b)); - } - if (requested.isEmpty()) throw new FormatException(); - r.readListEnd(); - // Read the end of the payload - r.readListEnd(); - if (!r.eof()) throw new FormatException(); - state = State.BUFFER_EMPTY; - // Build and return the request - return new Request(Collections.unmodifiableList(requested)); + return new Request(Collections.unmodifiableList(readMessageIds())); } public boolean hasSubscriptionAck() throws IOException { diff --git a/briar-core/src/org/briarproject/sync/PacketWriterImpl.java b/briar-core/src/org/briarproject/sync/PacketWriterImpl.java index 9c734a8eb228ffd7616bbffcda7c4ea382ff6bc7..449d40f19e233a0ac03fb14562276dc6716f84ae 100644 --- a/briar-core/src/org/briarproject/sync/PacketWriterImpl.java +++ b/briar-core/src/org/briarproject/sync/PacketWriterImpl.java @@ -1,5 +1,6 @@ package org.briarproject.sync; +import org.briarproject.api.UniqueId; import org.briarproject.api.data.BdfWriter; import org.briarproject.api.data.BdfWriterFactory; import org.briarproject.api.sync.Ack; @@ -19,12 +20,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import static org.briarproject.api.data.DataConstants.LIST_END_LENGTH; -import static org.briarproject.api.data.DataConstants.LIST_START_LENGTH; -import static org.briarproject.api.data.DataConstants.UNIQUE_ID_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.HEADER_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.PROTOCOL_VERSION; import static org.briarproject.api.sync.PacketTypes.ACK; import static org.briarproject.api.sync.PacketTypes.OFFER; import static org.briarproject.api.sync.PacketTypes.REQUEST; @@ -32,6 +27,9 @@ import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_ACK; import static org.briarproject.api.sync.PacketTypes.SUBSCRIPTION_UPDATE; import static org.briarproject.api.sync.PacketTypes.TRANSPORT_ACK; import static org.briarproject.api.sync.PacketTypes.TRANSPORT_UPDATE; +import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH; +import static org.briarproject.api.sync.SyncConstants.PACKET_HEADER_LENGTH; +import static org.briarproject.api.sync.SyncConstants.PROTOCOL_VERSION; // This class is not thread-safe class PacketWriterImpl implements PacketWriter { @@ -44,9 +42,9 @@ class PacketWriterImpl implements PacketWriter { PacketWriterImpl(BdfWriterFactory bdfWriterFactory, OutputStream out) { this.bdfWriterFactory = bdfWriterFactory; this.out = out; - header = new byte[HEADER_LENGTH]; + header = new byte[PACKET_HEADER_LENGTH]; header[0] = PROTOCOL_VERSION; - payload = new ByteArrayOutputStream(MAX_PAYLOAD_LENGTH); + payload = new ByteArrayOutputStream(MAX_PACKET_PAYLOAD_LENGTH); } public int getMaxMessagesForAck(long capacity) { @@ -62,10 +60,9 @@ class PacketWriterImpl implements PacketWriter { } private int getMaxMessagesForPacket(long capacity) { - int payload = (int) Math.min(capacity - HEADER_LENGTH, - MAX_PAYLOAD_LENGTH); - int overhead = LIST_START_LENGTH * 2 + LIST_END_LENGTH * 2; - return (payload - overhead) / UNIQUE_ID_LENGTH; + int payload = (int) Math.min(capacity - PACKET_HEADER_LENGTH, + MAX_PACKET_PAYLOAD_LENGTH); + return payload / UniqueId.LENGTH; } private void writePacket(byte packetType) throws IOException { @@ -77,13 +74,8 @@ class PacketWriterImpl implements PacketWriter { } public void writeAck(Ack a) throws IOException { - assert payload.size() == 0; - BdfWriter w = bdfWriterFactory.createWriter(payload); - w.writeListStart(); - w.writeListStart(); - for (MessageId m : a.getMessageIds()) w.writeRaw(m.getBytes()); - w.writeListEnd(); - w.writeListEnd(); + if (payload.size() != 0) throw new IllegalStateException(); + for (MessageId m : a.getMessageIds()) payload.write(m.getBytes()); writePacket(ACK); } @@ -95,29 +87,19 @@ class PacketWriterImpl implements PacketWriter { } public void writeOffer(Offer o) throws IOException { - assert payload.size() == 0; - BdfWriter w = bdfWriterFactory.createWriter(payload); - w.writeListStart(); - w.writeListStart(); - for (MessageId m : o.getMessageIds()) w.writeRaw(m.getBytes()); - w.writeListEnd(); - w.writeListEnd(); + if (payload.size() != 0) throw new IllegalStateException(); + for (MessageId m : o.getMessageIds()) payload.write(m.getBytes()); writePacket(OFFER); } public void writeRequest(Request r) throws IOException { - assert payload.size() == 0; - BdfWriter w = bdfWriterFactory.createWriter(payload); - w.writeListStart(); - w.writeListStart(); - for (MessageId m : r.getMessageIds()) w.writeRaw(m.getBytes()); - w.writeListEnd(); - w.writeListEnd(); + if (payload.size() != 0) throw new IllegalStateException(); + for (MessageId m : r.getMessageIds()) payload.write(m.getBytes()); writePacket(REQUEST); } public void writeSubscriptionAck(SubscriptionAck a) throws IOException { - assert payload.size() == 0; + if (payload.size() != 0) throw new IllegalStateException(); BdfWriter w = bdfWriterFactory.createWriter(payload); w.writeListStart(); w.writeInteger(a.getVersion()); @@ -127,14 +109,14 @@ class PacketWriterImpl implements PacketWriter { public void writeSubscriptionUpdate(SubscriptionUpdate u) throws IOException { - assert payload.size() == 0; + if (payload.size() != 0) throw new IllegalStateException(); BdfWriter w = bdfWriterFactory.createWriter(payload); w.writeListStart(); w.writeListStart(); for (Group g : u.getGroups()) { w.writeListStart(); - w.writeString(g.getName()); - w.writeRaw(g.getSalt()); + w.writeRaw(g.getClientId().getBytes()); + w.writeRaw(g.getDescriptor()); w.writeListEnd(); } w.writeListEnd(); @@ -144,7 +126,7 @@ class PacketWriterImpl implements PacketWriter { } public void writeTransportAck(TransportAck a) throws IOException { - assert payload.size() == 0; + if (payload.size() != 0) throw new IllegalStateException(); BdfWriter w = bdfWriterFactory.createWriter(payload); w.writeListStart(); w.writeString(a.getId().getString()); @@ -154,7 +136,7 @@ class PacketWriterImpl implements PacketWriter { } public void writeTransportUpdate(TransportUpdate u) throws IOException { - assert payload.size() == 0; + if (payload.size() != 0) throw new IllegalStateException(); BdfWriter w = bdfWriterFactory.createWriter(payload); w.writeListStart(); w.writeString(u.getId().getString()); diff --git a/briar-core/src/org/briarproject/sync/SigningConsumer.java b/briar-core/src/org/briarproject/sync/SigningConsumer.java deleted file mode 100644 index 4947cd659106c737b5bf889917cc3875379d8416..0000000000000000000000000000000000000000 --- a/briar-core/src/org/briarproject/sync/SigningConsumer.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.api.crypto.Signature; -import org.briarproject.api.data.Consumer; - -/** A consumer that passes its input through a signature. */ -class SigningConsumer implements Consumer { - - private final Signature signature; - - public SigningConsumer(Signature signature) { - this.signature = signature; - } - - public void write(byte b) { - signature.update(b); - } - - public void write(byte[] b, int off, int len) { - signature.update(b, off, len); - } -} diff --git a/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java index ace1d016cf1671a5acd9208b2638e34504563dda..f1504b9c825c674a7b8a618165e008fca1cca708 100644 --- a/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java +++ b/briar-core/src/org/briarproject/sync/SimplexOutgoingSession.java @@ -11,10 +11,10 @@ import org.briarproject.api.event.EventListener; import org.briarproject.api.event.ShutdownEvent; import org.briarproject.api.event.TransportRemovedEvent; import org.briarproject.api.sync.Ack; -import org.briarproject.api.sync.MessagingSession; import org.briarproject.api.sync.PacketWriter; import org.briarproject.api.sync.SubscriptionAck; import org.briarproject.api.sync.SubscriptionUpdate; +import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.TransportAck; import org.briarproject.api.sync.TransportUpdate; @@ -28,15 +28,15 @@ import java.util.logging.Logger; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH; /** - * An outgoing {@link org.briarproject.api.sync.MessagingSession - * MessagingSession} suitable for simplex transports. The session sends - * messages without offering them, and closes its output stream when there are - * no more packets to send. + * An outgoing {@link org.briarproject.api.sync.SyncSession SyncSession} + * suitable for simplex transports. The session sends messages without offering + * them first, and closes its output stream when there are no more packets to + * send. */ -class SimplexOutgoingSession implements MessagingSession, EventListener { +class SimplexOutgoingSession implements SyncSession, EventListener { private static final Logger LOG = Logger.getLogger(SimplexOutgoingSession.class.getName()); @@ -163,7 +163,7 @@ class SimplexOutgoingSession implements MessagingSession, EventListener { if (interrupted) return; try { Collection<byte[]> b = db.generateBatch(contactId, - MAX_PAYLOAD_LENGTH, maxLatency); + MAX_PACKET_PAYLOAD_LENGTH, maxLatency); if (LOG.isLoggable(INFO)) LOG.info("Generated batch: " + (b != null)); if (b == null) decrementOutstandingQueries(); diff --git a/briar-core/src/org/briarproject/sync/SubscriptionUpdateReader.java b/briar-core/src/org/briarproject/sync/SubscriptionUpdateReader.java index 8f4545ff8c761836814c63dfa7b2ee86287a25ee..38a297f214d4aa5938038ea2b4a3ad074f01eeb9 100644 --- a/briar-core/src/org/briarproject/sync/SubscriptionUpdateReader.java +++ b/briar-core/src/org/briarproject/sync/SubscriptionUpdateReader.java @@ -2,7 +2,6 @@ package org.briarproject.sync; import org.briarproject.api.FormatException; import org.briarproject.api.data.BdfReader; -import org.briarproject.api.data.Consumer; import org.briarproject.api.data.ObjectReader; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; @@ -15,8 +14,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_SUBSCRIPTIONS; +import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS; class SubscriptionUpdateReader implements ObjectReader<SubscriptionUpdate> { @@ -27,12 +25,7 @@ class SubscriptionUpdateReader implements ObjectReader<SubscriptionUpdate> { } public SubscriptionUpdate readObject(BdfReader r) throws IOException { - // Set up the reader - Consumer counting = new CountingConsumer(MAX_PAYLOAD_LENGTH); - r.addConsumer(counting); - // Read the start of the update r.readListStart(); - // Read the subscriptions, rejecting duplicates List<Group> groups = new ArrayList<Group>(); Set<GroupId> ids = new HashSet<GroupId>(); r.readListStart(); @@ -42,14 +35,9 @@ class SubscriptionUpdateReader implements ObjectReader<SubscriptionUpdate> { groups.add(g); } r.readListEnd(); - // Read the version number long version = r.readInteger(); if (version < 0) throw new FormatException(); - // Read the end of the update r.readListEnd(); - // Reset the reader - r.removeConsumer(counting); - // Build and return the subscription update groups = Collections.unmodifiableList(groups); return new SubscriptionUpdate(groups, version); } diff --git a/briar-core/src/org/briarproject/sync/SyncModule.java b/briar-core/src/org/briarproject/sync/SyncModule.java index 1caef5bf1ff92c78c0cfaf8bb2ba0b281f9ad853..cce031fd94941fe09e7059325a3118861052f308 100644 --- a/briar-core/src/org/briarproject/sync/SyncModule.java +++ b/briar-core/src/org/briarproject/sync/SyncModule.java @@ -3,19 +3,18 @@ package org.briarproject.sync; import com.google.inject.AbstractModule; import com.google.inject.Provides; -import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.data.ObjectReader; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.MessageFactory; -import org.briarproject.api.sync.MessageVerifier; -import org.briarproject.api.sync.MessagingSessionFactory; import org.briarproject.api.sync.PacketReaderFactory; import org.briarproject.api.sync.PacketWriterFactory; import org.briarproject.api.sync.SubscriptionUpdate; -import org.briarproject.api.sync.UnverifiedMessage; +import org.briarproject.api.sync.SyncSessionFactory; +import org.briarproject.api.sync.ValidationManager; import javax.inject.Singleton; @@ -26,28 +25,20 @@ public class SyncModule extends AbstractModule { bind(AuthorFactory.class).to(AuthorFactoryImpl.class); bind(GroupFactory.class).to(GroupFactoryImpl.class); bind(MessageFactory.class).to(MessageFactoryImpl.class); - bind(MessageVerifier.class).to(MessageVerifierImpl.class); bind(PacketReaderFactory.class).to(PacketReaderFactoryImpl.class); bind(PacketWriterFactory.class).to(PacketWriterFactoryImpl.class); - bind(MessagingSessionFactory.class).to( - MessagingSessionFactoryImpl.class).in(Singleton.class); + bind(SyncSessionFactory.class).to( + SyncSessionFactoryImpl.class).in(Singleton.class); } @Provides - ObjectReader<Author> getAuthorReader(CryptoComponent crypto) { - return new AuthorReader(crypto); + ObjectReader<Author> getAuthorReader(AuthorFactory authorFactory) { + return new AuthorReader(authorFactory); } @Provides - ObjectReader<Group> getGroupReader(CryptoComponent crypto) { - return new GroupReader(crypto); - } - - @Provides - ObjectReader<UnverifiedMessage> getMessageReader( - ObjectReader<Group> groupReader, - ObjectReader<Author> authorReader) { - return new MessageReader(groupReader, authorReader); + ObjectReader<Group> getGroupReader(GroupFactory groupFactory) { + return new GroupReader(groupFactory); } @Provides @@ -55,4 +46,11 @@ public class SyncModule extends AbstractModule { ObjectReader<Group> groupReader) { return new SubscriptionUpdateReader(groupReader); } + + @Provides @Singleton + ValidationManager getValidationManager(LifecycleManager lifecycleManager, + ValidationManagerImpl validationManager) { + lifecycleManager.register(validationManager); + return validationManager; + } } diff --git a/briar-core/src/org/briarproject/sync/MessagingSessionFactoryImpl.java b/briar-core/src/org/briarproject/sync/SyncSessionFactoryImpl.java similarity index 57% rename from briar-core/src/org/briarproject/sync/MessagingSessionFactoryImpl.java rename to briar-core/src/org/briarproject/sync/SyncSessionFactoryImpl.java index ef3317dc87487b8198eb88f392f69418eeeae522..5a4021b1d41ba409a1bc2394edbcad0dc0043f24 100644 --- a/briar-core/src/org/briarproject/sync/MessagingSessionFactoryImpl.java +++ b/briar-core/src/org/briarproject/sync/SyncSessionFactoryImpl.java @@ -2,17 +2,15 @@ package org.briarproject.sync; import org.briarproject.api.TransportId; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.crypto.CryptoExecutor; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.event.EventBus; -import org.briarproject.api.sync.MessageVerifier; -import org.briarproject.api.sync.MessagingSession; -import org.briarproject.api.sync.MessagingSessionFactory; import org.briarproject.api.sync.PacketReader; import org.briarproject.api.sync.PacketReaderFactory; import org.briarproject.api.sync.PacketWriter; import org.briarproject.api.sync.PacketWriterFactory; +import org.briarproject.api.sync.SyncSession; +import org.briarproject.api.sync.SyncSessionFactory; import org.briarproject.api.system.Clock; import java.io.InputStream; @@ -21,49 +19,44 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -class MessagingSessionFactoryImpl implements MessagingSessionFactory { +class SyncSessionFactoryImpl implements SyncSessionFactory { private final DatabaseComponent db; - private final Executor dbExecutor, cryptoExecutor; - private final MessageVerifier messageVerifier; + private final Executor dbExecutor; private final EventBus eventBus; private final Clock clock; private final PacketReaderFactory packetReaderFactory; private final PacketWriterFactory packetWriterFactory; @Inject - MessagingSessionFactoryImpl(DatabaseComponent db, - @DatabaseExecutor Executor dbExecutor, - @CryptoExecutor Executor cryptoExecutor, - MessageVerifier messageVerifier, EventBus eventBus, Clock clock, - PacketReaderFactory packetReaderFactory, + SyncSessionFactoryImpl(DatabaseComponent db, + @DatabaseExecutor Executor dbExecutor, EventBus eventBus, + Clock clock, PacketReaderFactory packetReaderFactory, PacketWriterFactory packetWriterFactory) { this.db = db; this.dbExecutor = dbExecutor; - this.cryptoExecutor = cryptoExecutor; - this.messageVerifier = messageVerifier; this.eventBus = eventBus; this.clock = clock; this.packetReaderFactory = packetReaderFactory; this.packetWriterFactory = packetWriterFactory; } - public MessagingSession createIncomingSession(ContactId c, TransportId t, + public SyncSession createIncomingSession(ContactId c, TransportId t, InputStream in) { PacketReader packetReader = packetReaderFactory.createPacketReader(in); - return new IncomingSession(db, dbExecutor, cryptoExecutor, eventBus, - messageVerifier, c, t, packetReader); + return new IncomingSession(db, dbExecutor, eventBus, c, t, + packetReader); } - public MessagingSession createSimplexOutgoingSession(ContactId c, - TransportId t, int maxLatency, OutputStream out) { + public SyncSession createSimplexOutgoingSession(ContactId c, TransportId t, + int maxLatency, OutputStream out) { PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out); return new SimplexOutgoingSession(db, dbExecutor, eventBus, c, t, maxLatency, packetWriter); } - public MessagingSession createDuplexOutgoingSession(ContactId c, - TransportId t, int maxLatency, int maxIdleTime, OutputStream out) { + public SyncSession createDuplexOutgoingSession(ContactId c, TransportId t, + int maxLatency, int maxIdleTime, OutputStream out) { PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out); return new DuplexOutgoingSession(db, dbExecutor, eventBus, clock, c, t, maxLatency, maxIdleTime, packetWriter); diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..68040be941b37f2aa0edf7ccef1a59f5ec91fd78 --- /dev/null +++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java @@ -0,0 +1,162 @@ +package org.briarproject.sync; + +import com.google.inject.Inject; + +import org.briarproject.api.UniqueId; +import org.briarproject.api.crypto.CryptoExecutor; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DatabaseExecutor; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; +import org.briarproject.api.db.NoSuchMessageException; +import org.briarproject.api.db.NoSuchSubscriptionException; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.MessageAddedEvent; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageValidator; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.util.ByteUtils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; + +class ValidationManagerImpl implements ValidationManager, EventListener { + + private static final Logger LOG = + Logger.getLogger(ValidationManagerImpl.class.getName()); + + private final DatabaseComponent db; + private final Executor dbExecutor; + private final Executor cryptoExecutor; + private final EventBus eventBus; + private final Map<ClientId, MessageValidator> validators; + + @Inject + ValidationManagerImpl(DatabaseComponent db, + @DatabaseExecutor Executor dbExecutor, + @CryptoExecutor Executor cryptoExecutor, EventBus eventBus) { + this.db = db; + this.dbExecutor = dbExecutor; + this.cryptoExecutor = cryptoExecutor; + this.eventBus = eventBus; + validators = new ConcurrentHashMap<ClientId, MessageValidator>(); + } + + @Override + public boolean start() { + eventBus.addListener(this); + return true; + } + + @Override + public boolean stop() { + eventBus.removeListener(this); + return true; + } + + @Override + public void setMessageValidator(ClientId c, MessageValidator v) { + validators.put(c, v); + getMessagesToValidate(c); + } + + private void getMessagesToValidate(final ClientId c) { + dbExecutor.execute(new Runnable() { + public void run() { + try { + // TODO: Don't do all of this in a single DB task + for (MessageId id : db.getMessagesToValidate(c)) { + try { + Message m = parseMessage(id, db.getRawMessage(id)); + validateMessage(m, c); + } catch (NoSuchMessageException e) { + LOG.info("Message removed before validation"); + } + } + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private Message parseMessage(MessageId id, byte[] raw) { + if (raw.length <= MESSAGE_HEADER_LENGTH) + throw new IllegalArgumentException(); + byte[] groupId = new byte[UniqueId.LENGTH]; + System.arraycopy(raw, 0, groupId, 0, UniqueId.LENGTH); + long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH); + return new Message(id, new GroupId(groupId), timestamp, raw); + } + + private void validateMessage(final Message m, final ClientId c) { + cryptoExecutor.execute(new Runnable() { + public void run() { + MessageValidator v = validators.get(c); + if (v == null) { + LOG.warning("No validator"); + } else { + Metadata meta = v.validateMessage(m); + storeValidationResult(m, c, meta); + } + } + }); + } + + private void storeValidationResult(final Message m, final ClientId c, + final Metadata meta) { + dbExecutor.execute(new Runnable() { + public void run() { + try { + if (meta == null) { + db.setMessageValidity(m, c, false); + } else { + db.mergeMessageMetadata(m.getId(), meta); + db.setMessageValidity(m, c, true); + } + } catch (NoSuchMessageException e) { + LOG.info("Message removed during validation"); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof MessageAddedEvent) { + MessageAddedEvent m = (MessageAddedEvent) e; + // Validate the message if it wasn't created locally + if (m.getContactId() != null) loadClientId(m.getMessage()); + } + } + + private void loadClientId(final Message m) { + dbExecutor.execute(new Runnable() { + public void run() { + try { + ClientId c = db.getGroup(m.getGroupId()).getClientId(); + validateMessage(m, c); + } catch (NoSuchSubscriptionException e) { + LOG.info("Group removed before validation"); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } +} diff --git a/briar-core/src/org/briarproject/util/ByteUtils.java b/briar-core/src/org/briarproject/util/ByteUtils.java index 9213e65d8931072c3a30af52935bf1f330c2f1fa..061a4780db9d5bf5c9f6eee71e87ad3c6f06b8df 100644 --- a/briar-core/src/org/briarproject/util/ByteUtils.java +++ b/briar-core/src/org/briarproject/util/ByteUtils.java @@ -70,6 +70,19 @@ public class ByteUtils { | (src[offset + 3] & 0xFFL); } + public static long readUint64(byte[] src, int offset) { + if (src.length < offset + INT_64_BYTES) + throw new IllegalArgumentException(); + return ((src[offset] & 0xFFL) << 56) + | ((src[offset + 1] & 0xFFL) << 48) + | ((src[offset + 2] & 0xFFL) << 40) + | ((src[offset + 3] & 0xFFL) << 32) + | ((src[offset + 4] & 0xFFL) << 24) + | ((src[offset + 5] & 0xFFL) << 16) + | ((src[offset + 6] & 0xFFL) << 8) + | (src[offset + 7] & 0xFFL); + } + public static int readUint(byte[] src, int bits) { if (src.length << 3 < bits) throw new IllegalArgumentException(); int dest = 0; diff --git a/briar-tests/build.xml b/briar-tests/build.xml index 7508f612df21efbbe4906b074b35fa519479b5c5..408832ae8b3c16c6543378cffc8f1e64198945fa 100644 --- a/briar-tests/build.xml +++ b/briar-tests/build.xml @@ -125,7 +125,6 @@ <test name='org.briarproject.plugins.modem.ModemPluginTest'/> <test name='org.briarproject.plugins.tcp.LanTcpPluginTest'/> <test name='org.briarproject.sync.ConstantsTest'/> - <test name='org.briarproject.sync.ConsumersTest'/> <test name='org.briarproject.sync.PacketReaderImplTest'/> <test name='org.briarproject.sync.SimplexMessagingIntegrationTest'/> <test name='org.briarproject.sync.SimplexOutgoingSessionTest'/> diff --git a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java index 807b04c185444162b03a2d81189e346cd2079695..98975f205f2e6cedebc0dff4c377aafe35e22e62 100644 --- a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java +++ b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java @@ -6,18 +6,14 @@ import com.google.inject.Injector; import org.briarproject.api.TransportId; import org.briarproject.api.TransportProperties; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.crypto.CryptoComponent; -import org.briarproject.api.crypto.KeyPair; import org.briarproject.api.crypto.SecretKey; -import org.briarproject.api.identity.Author; -import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.sync.Ack; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.Message; import org.briarproject.api.sync.MessageFactory; import org.briarproject.api.sync.MessageId; -import org.briarproject.api.sync.MessageVerifier; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.PacketReader; import org.briarproject.api.sync.PacketReaderFactory; @@ -26,7 +22,6 @@ import org.briarproject.api.sync.PacketWriterFactory; import org.briarproject.api.sync.Request; import org.briarproject.api.sync.SubscriptionUpdate; import org.briarproject.api.sync.TransportUpdate; -import org.briarproject.api.sync.UnverifiedMessage; import org.briarproject.api.transport.StreamContext; import org.briarproject.api.transport.StreamReaderFactory; import org.briarproject.api.transport.StreamWriterFactory; @@ -46,6 +41,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -58,7 +54,6 @@ public class ProtocolIntegrationTest extends BriarTestCase { private final StreamWriterFactory streamWriterFactory; private final PacketReaderFactory packetReaderFactory; private final PacketWriterFactory packetWriterFactory; - private final MessageVerifier messageVerifier; private final ContactId contactId; private final SecretKey tagKey, headerKey; @@ -78,29 +73,22 @@ public class ProtocolIntegrationTest extends BriarTestCase { streamWriterFactory = i.getInstance(StreamWriterFactory.class); packetReaderFactory = i.getInstance(PacketReaderFactory.class); packetWriterFactory = i.getInstance(PacketWriterFactory.class); - messageVerifier = i.getInstance(MessageVerifier.class); contactId = new ContactId(234); // Create the transport keys tagKey = TestUtils.createSecretKey(); headerKey = TestUtils.createSecretKey(); // Create a group GroupFactory groupFactory = i.getInstance(GroupFactory.class); - group = groupFactory.createGroup("Group"); - // Create an author - AuthorFactory authorFactory = i.getInstance(AuthorFactory.class); - CryptoComponent crypto = i.getInstance(CryptoComponent.class); - KeyPair authorKeyPair = crypto.generateSignatureKeyPair(); - Author author = authorFactory.createAuthor("Alice", - authorKeyPair.getPublic().getEncoded()); - // Create two messages to the group: one anonymous, one pseudonymous + ClientId clientId = new ClientId(TestUtils.getRandomId()); + byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; + group = groupFactory.createGroup(clientId, descriptor); + // Add two messages to the group MessageFactory messageFactory = i.getInstance(MessageFactory.class); - String contentType = "text/plain"; long timestamp = System.currentTimeMillis(); String messageBody = "Hello world"; - message = messageFactory.createAnonymousMessage(null, group, - "text/plain", timestamp, messageBody.getBytes("UTF-8")); - message1 = messageFactory.createPseudonymousMessage(null, group, - author, authorKeyPair.getPrivate(), contentType, timestamp, + message = messageFactory.createMessage(group.getId(), timestamp, + messageBody.getBytes("UTF-8")); + message1 = messageFactory.createMessage(group.getId(), timestamp, messageBody.getBytes("UTF-8")); messageIds = Arrays.asList(message.getId(), message1.getId()); // Create some transport properties @@ -125,8 +113,8 @@ public class ProtocolIntegrationTest extends BriarTestCase { packetWriter.writeAck(new Ack(messageIds)); - packetWriter.writeMessage(message.getSerialised()); - packetWriter.writeMessage(message1.getSerialised()); + packetWriter.writeMessage(message.getRaw()); + packetWriter.writeMessage(message1.getRaw()); packetWriter.writeOffer(new Offer(messageIds)); @@ -163,11 +151,11 @@ public class ProtocolIntegrationTest extends BriarTestCase { // Read and verify the messages assertTrue(packetReader.hasMessage()); - UnverifiedMessage m = packetReader.readMessage(); - checkMessageEquality(message, messageVerifier.verifyMessage(m)); + Message m = packetReader.readMessage(); + checkMessageEquality(message, m); assertTrue(packetReader.hasMessage()); m = packetReader.readMessage(); - checkMessageEquality(message1, messageVerifier.verifyMessage(m)); + checkMessageEquality(message1, m); assertFalse(packetReader.hasMessage()); // Read the offer @@ -198,10 +186,7 @@ public class ProtocolIntegrationTest extends BriarTestCase { private void checkMessageEquality(Message m1, Message m2) { assertEquals(m1.getId(), m2.getId()); - assertEquals(m1.getParent(), m2.getParent()); - assertEquals(m1.getGroup(), m2.getGroup()); - assertEquals(m1.getAuthor(), m2.getAuthor()); assertEquals(m1.getTimestamp(), m2.getTimestamp()); - assertArrayEquals(m1.getSerialised(), m2.getSerialised()); + assertArrayEquals(m1.getRaw(), m2.getRaw()); } } diff --git a/briar-tests/src/org/briarproject/TestMessage.java b/briar-tests/src/org/briarproject/TestMessage.java deleted file mode 100644 index a8ffc3cb3a3e45ab2cb53c3146e93d0a55bffd9a..0000000000000000000000000000000000000000 --- a/briar-tests/src/org/briarproject/TestMessage.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.briarproject; - -import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.Group; -import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageId; - -public class TestMessage implements Message { - - private final MessageId id, parent; - private final Group group; - private final Author author; - private final String contentType; - private final long timestamp; - private final byte[] raw; - private final int bodyStart, bodyLength; - - public TestMessage(MessageId id, MessageId parent, Group group, - Author author, String contentType, long timestamp, byte[] raw) { - this(id, parent, group, author, contentType, timestamp, raw, 0, - raw.length); - } - - public TestMessage(MessageId id, MessageId parent, Group group, - Author author, String contentType, long timestamp, byte[] raw, - int bodyStart, int bodyLength) { - this.id = id; - this.parent = parent; - this.group = group; - this.author = author; - this.contentType = contentType; - this.timestamp = timestamp; - this.raw = raw; - this.bodyStart = bodyStart; - this.bodyLength = bodyLength; - } - - public MessageId getId() { - return id; - } - - public MessageId getParent() { - return parent; - } - - public Group getGroup() { - return group; - } - - public Author getAuthor() { - return author; - } - - public String getContentType() { - return contentType; - } - - public long getTimestamp() { - return timestamp; - } - - public byte[] getSerialised() { - return raw; - } - - public int getBodyStart() { - return bodyStart; - } - - public int getBodyLength() { - return bodyLength; - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object o) { - return o instanceof Message && id.equals(((Message)o).getId()); - } -} diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java index ba44df11673c3571d85fcf4bdacd916c61e67580..a6bd31ab33e861eda3e7a2803a8fa01b9126a80c 100644 --- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java +++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java @@ -1,7 +1,6 @@ package org.briarproject.db; import org.briarproject.BriarTestCase; -import org.briarproject.TestMessage; import org.briarproject.TestUtils; import org.briarproject.api.Settings; import org.briarproject.api.TransportId; @@ -10,8 +9,11 @@ import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.SecretKey; import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.MessageExistsException; +import org.briarproject.api.db.Metadata; import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.db.NoSuchLocalAuthorException; +import org.briarproject.api.db.NoSuchMessageException; import org.briarproject.api.db.NoSuchSubscriptionException; import org.briarproject.api.db.NoSuchTransportException; import org.briarproject.api.event.ContactAddedEvent; @@ -25,6 +27,7 @@ import org.briarproject.api.event.MessageAddedEvent; import org.briarproject.api.event.MessageRequestedEvent; import org.briarproject.api.event.MessageToAckEvent; import org.briarproject.api.event.MessageToRequestEvent; +import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.SubscriptionAddedEvent; @@ -34,6 +37,7 @@ import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.lifecycle.ShutdownManager; import org.briarproject.api.sync.Ack; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; @@ -56,7 +60,7 @@ import java.util.Collection; import java.util.Collections; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.GROUP_SALT_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -66,28 +70,31 @@ import static org.junit.Assert.fail; public class DatabaseComponentImplTest extends BriarTestCase { - protected final Object txn = new Object(); - protected final GroupId groupId; - protected final Group group; - protected final AuthorId authorId; - protected final Author author; - protected final AuthorId localAuthorId; - protected final LocalAuthor localAuthor; - protected final MessageId messageId, messageId1; - protected final String contentType; - protected final long timestamp; - protected final int size; - protected final byte[] raw; - protected final Message message, message1; - protected final TransportId transportId; - protected final TransportProperties transportProperties; - protected final int maxLatency; - protected final ContactId contactId; - protected final Contact contact; + private final Object txn = new Object(); + private final ClientId clientId; + private final GroupId groupId; + private final Group group; + private final AuthorId authorId; + private final Author author; + private final AuthorId localAuthorId; + private final LocalAuthor localAuthor; + private final MessageId messageId, messageId1; + private final int size; + private final byte[] raw; + private final Message message; + private final Metadata metadata; + private final TransportId transportId; + private final TransportProperties transportProperties; + private final int maxLatency; + private final ContactId contactId; + private final Contact contact; public DatabaseComponentImplTest() { + clientId = new ClientId(TestUtils.getRandomId()); groupId = new GroupId(TestUtils.getRandomId()); - group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH]); + ClientId clientId = new ClientId(TestUtils.getRandomId()); + byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; + group = new Group(groupId, clientId, descriptor); authorId = new AuthorId(TestUtils.getRandomId()); author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); localAuthorId = new AuthorId(TestUtils.getRandomId()); @@ -95,14 +102,12 @@ public class DatabaseComponentImplTest extends BriarTestCase { new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100], 1234); messageId = new MessageId(TestUtils.getRandomId()); messageId1 = new MessageId(TestUtils.getRandomId()); - contentType = "text/plain"; - timestamp = System.currentTimeMillis(); + long timestamp = System.currentTimeMillis(); size = 1234; raw = new byte[size]; - message = new TestMessage(messageId, null, group, author, contentType, - timestamp, raw); - message1 = new TestMessage(messageId1, messageId, group, null, - contentType, timestamp, raw); + message = new Message(messageId, groupId, timestamp, raw); + metadata = new Metadata(); + metadata.put("foo", new byte[] {'b', 'a', 'r'}); transportId = new TransportId("id"); transportProperties = new TransportProperties(Collections.singletonMap( "bar", "baz")); @@ -125,9 +130,9 @@ public class DatabaseComponentImplTest extends BriarTestCase { final ShutdownManager shutdown = context.mock(ShutdownManager.class); final EventBus eventBus = context.mock(EventBus.class); context.checking(new Expectations() {{ - exactly(11).of(database).startTransaction(); + exactly(10).of(database).startTransaction(); will(returnValue(txn)); - exactly(11).of(database).commitTransaction(txn); + exactly(10).of(database).commitTransaction(txn); // open() oneOf(database).open(); will(returnValue(false)); @@ -161,13 +166,8 @@ public class DatabaseComponentImplTest extends BriarTestCase { // addGroup() again oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); - // getMessageHeaders() - oneOf(database).containsGroup(txn, groupId); - will(returnValue(true)); - oneOf(database).getMessageHeaders(txn, groupId); - will(returnValue(Collections.emptyList())); // getGroups() - oneOf(database).getGroups(txn); + oneOf(database).getGroups(txn, clientId); will(returnValue(Collections.singletonList(group))); // removeGroup() oneOf(database).containsGroup(txn, groupId); @@ -182,8 +182,6 @@ public class DatabaseComponentImplTest extends BriarTestCase { // removeContact() oneOf(database).containsContact(txn, contactId); will(returnValue(true)); - oneOf(database).getInboxGroupId(txn, contactId); - will(returnValue(null)); oneOf(database).removeContact(txn, contactId); oneOf(eventBus).broadcast(with(any(ContactRemovedEvent.class))); // removeLocalAuthor() @@ -208,8 +206,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { db.getRemoteProperties(transportId)); db.addGroup(group); // First time - listeners called db.addGroup(group); // Second time - not called - assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId)); - assertEquals(Collections.singletonList(group), db.getGroups()); + assertEquals(Collections.singletonList(group), db.getGroups(clientId)); db.removeGroup(group); db.removeContact(contactId); db.removeLocalAuthor(localAuthorId); @@ -230,14 +227,17 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(txn)); oneOf(database).containsMessage(txn, messageId); will(returnValue(true)); - oneOf(database).containsGroup(txn, groupId); - will(returnValue(true)); - oneOf(database).commitTransaction(txn); + oneOf(database).abortTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); - db.addLocalMessage(message); + try { + db.addLocalMessage(message, clientId, metadata); + fail(); + } catch (MessageExistsException expected) { + // Expected + } context.assertIsSatisfied(); } @@ -257,12 +257,17 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(false)); oneOf(database).containsGroup(txn, groupId); will(returnValue(false)); - oneOf(database).commitTransaction(txn); + oneOf(database).abortTransaction(txn); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); - db.addLocalMessage(message); + try { + db.addLocalMessage(message, clientId, metadata); + fail(); + } catch (NoSuchSubscriptionException expected) { + // Expected + } context.assertIsSatisfied(); } @@ -282,7 +287,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { oneOf(database).containsGroup(txn, groupId); will(returnValue(true)); oneOf(database).addMessage(txn, message, true); - oneOf(database).setReadFlag(txn, messageId, true); + oneOf(database).mergeMessageMetadata(txn, messageId, metadata); oneOf(database).getVisibility(txn, groupId); will(returnValue(Collections.singletonList(contactId))); oneOf(database).getContactIds(txn); @@ -291,13 +296,14 @@ public class DatabaseComponentImplTest extends BriarTestCase { will(returnValue(false)); oneOf(database).addStatus(txn, contactId, messageId, false, false); oneOf(database).commitTransaction(txn); - // The message was added, so the listener should be called + // The message was added, so the listeners should be called oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class))); + oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class))); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); - db.addLocalMessage(message); + db.addLocalMessage(message, clientId, metadata); context.assertIsSatisfied(); } @@ -385,7 +391,14 @@ public class DatabaseComponentImplTest extends BriarTestCase { } try { - db.getInboxGroupId(contactId); + db.getMessageStatus(contactId, groupId); + fail(); + } catch (NoSuchContactException expected) { + // Expected + } + + try { + db.getMessageStatus(contactId, messageId); fail(); } catch (NoSuchContactException expected) { // Expected @@ -469,13 +482,6 @@ public class DatabaseComponentImplTest extends BriarTestCase { // Expected } - try { - db.setInboxGroup(contactId, group); - fail(); - } catch (NoSuchContactException expected) { - // Expected - } - context.assertIsSatisfied(); } @@ -540,6 +546,9 @@ public class DatabaseComponentImplTest extends BriarTestCase { exactly(5).of(database).containsGroup(txn, groupId); will(returnValue(false)); exactly(5).of(database).abortTransaction(txn); + // This is needed for getMessageStatus() to proceed + exactly(1).of(database).containsContact(txn, contactId); + will(returnValue(true)); }}); DatabaseComponent db = createDatabaseComponent(database, eventBus, shutdown); @@ -552,7 +561,7 @@ public class DatabaseComponentImplTest extends BriarTestCase { } try { - db.getMessageHeaders(groupId); + db.getMessageStatus(contactId, groupId); fail(); } catch (NoSuchSubscriptionException expected) { // Expected @@ -582,6 +591,59 @@ public class DatabaseComponentImplTest extends BriarTestCase { context.assertIsSatisfied(); } + @Test + public void testVariousMethodsThrowExceptionIfMessageIsMissing() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final EventBus eventBus = context.mock(EventBus.class); + context.checking(new Expectations() {{ + // Check whether the message is in the DB (which it's not) + exactly(4).of(database).startTransaction(); + will(returnValue(txn)); + exactly(4).of(database).containsMessage(txn, messageId); + will(returnValue(false)); + exactly(4).of(database).abortTransaction(txn); + // This is needed for getMessageStatus() to proceed + exactly(1).of(database).containsContact(txn, contactId); + will(returnValue(true)); + }}); + DatabaseComponent db = createDatabaseComponent(database, eventBus, + shutdown); + + try { + db.getRawMessage(messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } + + try { + db.getMessageMetadata(messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } + + try { + db.getMessageStatus(contactId, messageId); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } + + try { + db.mergeMessageMetadata(messageId, metadata); + fail(); + } catch (NoSuchMessageException expected) { + // Expected + } + + context.assertIsSatisfied(); + } + @Test public void testVariousMethodsThrowExceptionIfTransportIsMissing() throws Exception { diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java index 35076d6f26c8a14b13b6aa858080d5c5dbce5af6..f034e3f4be0165ea7d47cce36fd08af602427076 100644 --- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java +++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java @@ -2,21 +2,22 @@ package org.briarproject.db; import org.briarproject.BriarTestCase; import org.briarproject.TestDatabaseConfig; -import org.briarproject.TestMessage; import org.briarproject.TestUtils; import org.briarproject.api.TransportId; import org.briarproject.api.TransportProperties; import org.briarproject.api.contact.ContactId; import org.briarproject.api.crypto.SecretKey; import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageHeader; import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageStatus; import org.briarproject.api.transport.IncomingKeys; import org.briarproject.api.transport.OutgoingKeys; import org.briarproject.api.transport.TransportKeys; @@ -42,13 +43,11 @@ import java.util.concurrent.atomic.AtomicBoolean; import static java.util.concurrent.TimeUnit.SECONDS; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; -import static org.briarproject.api.sync.MessageHeader.State.STORED; -import static org.briarproject.api.sync.MessagingConstants.GROUP_SALT_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -59,13 +58,13 @@ public class H2DatabaseTest extends BriarTestCase { private final File testDir = TestUtils.getTestDirectory(); private final Random random = new Random(); + private final ClientId clientId; private final GroupId groupId; private final Group group; private final Author author; private final AuthorId localAuthorId; private final LocalAuthor localAuthor; private final MessageId messageId; - private final String contentType; private final long timestamp; private final int size; private final byte[] raw; @@ -74,21 +73,21 @@ public class H2DatabaseTest extends BriarTestCase { private final ContactId contactId; public H2DatabaseTest() throws Exception { + clientId = new ClientId(TestUtils.getRandomId()); groupId = new GroupId(TestUtils.getRandomId()); - group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH]); + byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; + group = new Group(groupId, clientId, descriptor); AuthorId authorId = new AuthorId(TestUtils.getRandomId()); author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); localAuthorId = new AuthorId(TestUtils.getRandomId()); localAuthor = new LocalAuthor(localAuthorId, "Bob", new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100], 1234); messageId = new MessageId(TestUtils.getRandomId()); - contentType = "text/plain"; timestamp = System.currentTimeMillis(); size = 1234; raw = new byte[size]; random.nextBytes(raw); - message = new TestMessage(messageId, null, group, author, contentType, - timestamp, raw); + message = new Message(messageId, groupId, timestamp, raw); transportId = new TransportId("id"); contactId = new ContactId(1); } @@ -309,8 +308,7 @@ public class H2DatabaseTest extends BriarTestCase { // Add some messages to ack MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new TestMessage(messageId1, null, group, author, - contentType, timestamp, raw); + Message message1 = new Message(messageId1, groupId, timestamp, raw); db.addMessage(txn, message, true); db.addStatus(txn, contactId, messageId, false, true); db.raiseAckFlag(txn, contactId, messageId); @@ -404,10 +402,9 @@ public class H2DatabaseTest extends BriarTestCase { @Test public void testGetFreeSpace() throws Exception { - byte[] largeBody = new byte[ONE_MEGABYTE]; + byte[] largeBody = new byte[MAX_MESSAGE_LENGTH]; for (int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i; - Message message = new TestMessage(messageId, null, group, author, - contentType, timestamp, largeBody); + Message message = new Message(messageId, groupId, timestamp, largeBody); Database<Connection> db = open(false); // Sanity check: there should be enough space on disk for this test @@ -718,343 +715,15 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } - @Test - public void testGetParentWithNoParent() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Subscribe to a group - db.addGroup(txn, group); - - // A message with no parent should return null - MessageId childId = new MessageId(TestUtils.getRandomId()); - Message child = new TestMessage(childId, null, group, null, contentType, - timestamp, raw); - db.addMessage(txn, child, true); - assertTrue(db.containsMessage(txn, childId)); - assertNull(db.getParent(txn, childId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetParentWithAbsentParent() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Subscribe to a group - db.addGroup(txn, group); - - // A message with an absent parent should return null - MessageId childId = new MessageId(TestUtils.getRandomId()); - MessageId parentId = new MessageId(TestUtils.getRandomId()); - Message child = new TestMessage(childId, parentId, group, null, - contentType, timestamp, raw); - db.addMessage(txn, child, true); - assertTrue(db.containsMessage(txn, childId)); - assertFalse(db.containsMessage(txn, parentId)); - assertNull(db.getParent(txn, childId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetParentWithParentInAnotherGroup() throws Exception { - GroupId groupId1 = new GroupId(TestUtils.getRandomId()); - Group group1 = new Group(groupId1, "Another group", - new byte[GROUP_SALT_LENGTH]); - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Subscribe to two groups - db.addGroup(txn, group); - db.addGroup(txn, group1); - - // A message with a parent in another group should return null - MessageId childId = new MessageId(TestUtils.getRandomId()); - MessageId parentId = new MessageId(TestUtils.getRandomId()); - Message child = new TestMessage(childId, parentId, group, null, - contentType, timestamp, raw); - Message parent = new TestMessage(parentId, null, group1, null, - contentType, timestamp, raw); - db.addMessage(txn, child, true); - db.addMessage(txn, parent, true); - assertTrue(db.containsMessage(txn, childId)); - assertTrue(db.containsMessage(txn, parentId)); - assertNull(db.getParent(txn, childId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetParentWithParentInSameGroup() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Subscribe to a group - db.addGroup(txn, group); - - // A message with a parent in the same group should return the parent - MessageId childId = new MessageId(TestUtils.getRandomId()); - MessageId parentId = new MessageId(TestUtils.getRandomId()); - Message child = new TestMessage(childId, parentId, group, null, - contentType, timestamp, raw); - Message parent = new TestMessage(parentId, null, group, null, - contentType, timestamp, raw); - db.addMessage(txn, child, true); - db.addMessage(txn, parent, true); - assertTrue(db.containsMessage(txn, childId)); - assertTrue(db.containsMessage(txn, parentId)); - assertEquals(parentId, db.getParent(txn, childId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetMessageBody() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and subscribe to a group - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addGroup(txn, group); - - // Store a couple of messages - int bodyLength = raw.length - 20; - Message message = new TestMessage(messageId, null, group, null, - contentType, timestamp, raw, 5, bodyLength); - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new TestMessage(messageId1, null, group, null, - contentType, timestamp, raw, 10, bodyLength); - db.addMessage(txn, message, true); - db.addMessage(txn, message1, true); - - // Calculate the expected message bodies - byte[] expectedBody = new byte[bodyLength]; - System.arraycopy(raw, 5, expectedBody, 0, bodyLength); - assertFalse(Arrays.equals(expectedBody, new byte[bodyLength])); - byte[] expectedBody1 = new byte[bodyLength]; - System.arraycopy(raw, 10, expectedBody1, 0, bodyLength); - System.arraycopy(raw, 10, expectedBody1, 0, bodyLength); - - // Retrieve the raw messages - assertArrayEquals(raw, db.getRawMessage(txn, messageId)); - assertArrayEquals(raw, db.getRawMessage(txn, messageId1)); - - // Retrieve the message bodies - byte[] body = db.getMessageBody(txn, messageId); - assertArrayEquals(expectedBody, body); - byte[] body1 = db.getMessageBody(txn, messageId1); - assertArrayEquals(expectedBody1, body1); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetMessageHeaders() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Subscribe to a group - db.addGroup(txn, group); - - // Store a couple of messages - db.addMessage(txn, message, true); - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - MessageId parentId = new MessageId(TestUtils.getRandomId()); - long timestamp1 = System.currentTimeMillis(); - Message message1 = new TestMessage(messageId1, parentId, group, author, - contentType, timestamp1, raw); - db.addMessage(txn, message1, true); - // Mark one of the messages read - db.setReadFlag(txn, messageId, true); - - // Retrieve the message headers (order is undefined) - Collection<MessageHeader> headers = db.getMessageHeaders(txn, groupId); - assertEquals(2, headers.size()); - boolean firstFound = false, secondFound = false; - for (MessageHeader header : headers) { - if (messageId.equals(header.getId())) { - assertHeadersMatch(message, header); - assertTrue(header.isRead()); - firstFound = true; - } else if (messageId1.equals(header.getId())) { - assertHeadersMatch(message1, header); - assertFalse(header.isRead()); - secondFound = true; - } else { - fail(); - } - } - // Both the headers should have been retrieved - assertTrue(firstFound); - assertTrue(secondFound); - - db.commitTransaction(txn); - db.close(); - } - - private void assertHeadersMatch(Message m, MessageHeader h) { - assertEquals(m.getId(), h.getId()); - if (m.getParent() == null) assertNull(h.getParent()); - else assertEquals(m.getParent(), h.getParent()); - assertEquals(m.getGroup().getId(), h.getGroupId()); - if (m.getAuthor() == null) assertNull(h.getAuthor()); - else assertEquals(m.getAuthor(), h.getAuthor()); - assertEquals(m.getContentType(), h.getContentType()); - assertEquals(m.getTimestamp(), h.getTimestamp()); - } - - @Test - public void testAuthorStatus() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and subscribe to a group - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addGroup(txn, group); - - // Store a message from the contact - status VERIFIED - db.addMessage(txn, message, true); - AuthorId authorId1 = new AuthorId(TestUtils.getRandomId()); - // Store a message from an unknown author - status UNKNOWN - Author author1 = new Author(authorId1, "Bob", - new byte[MAX_PUBLIC_KEY_LENGTH]); - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new TestMessage(messageId1, null, group, author1, - contentType, timestamp, raw); - db.addMessage(txn, message1, true); - // Store an anonymous message - status ANONYMOUS - MessageId messageId2 = new MessageId(TestUtils.getRandomId()); - Message message2 = new TestMessage(messageId2, null, group, null, - contentType, timestamp, raw); - db.addMessage(txn, message2, true); - - // Retrieve the message headers (order is undefined) - Collection<MessageHeader> headers = db.getMessageHeaders(txn, groupId); - assertEquals(3, headers.size()); - boolean firstFound = false, secondFound = false, thirdFound = false; - for (MessageHeader header : headers) { - if (messageId.equals(header.getId())) { - assertHeadersMatch(message, header); - assertEquals(Author.Status.VERIFIED, header.getAuthorStatus()); - firstFound = true; - } else if (messageId1.equals(header.getId())) { - assertHeadersMatch(message1, header); - assertEquals(Author.Status.UNKNOWN, header.getAuthorStatus()); - secondFound = true; - } else if (messageId2.equals(header.getId())) { - assertHeadersMatch(message2, header); - assertEquals(Author.Status.ANONYMOUS, header.getAuthorStatus()); - thirdFound = true; - } else { - fail(); - } - } - // All of the headers should have been retrieved - assertTrue(firstFound); - assertTrue(secondFound); - assertTrue(thirdFound); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testReadFlag() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Subscribe to a group and store a message - db.addGroup(txn, group); - db.addMessage(txn, message, true); - - // The message should be unread by default - assertFalse(db.getReadFlag(txn, messageId)); - // Mark the message read - db.setReadFlag(txn, messageId, true); - // The message should be read - assertTrue(db.getReadFlag(txn, messageId)); - // Mark the message unread - db.setReadFlag(txn, messageId, false); - // The message should be unread - assertFalse(db.getReadFlag(txn, messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetUnreadMessageCounts() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Subscribe to a couple of groups - db.addGroup(txn, group); - GroupId groupId1 = new GroupId(TestUtils.getRandomId()); - Group group1 = new Group(groupId1, "Another group", - new byte[GROUP_SALT_LENGTH]); - db.addGroup(txn, group1); - - // Store two messages in the first group - db.addMessage(txn, message, true); - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new TestMessage(messageId1, null, group, author, - contentType, timestamp, raw); - db.addMessage(txn, message1, true); - - // Store one message in the second group - MessageId messageId2 = new MessageId(TestUtils.getRandomId()); - Message message2 = new TestMessage(messageId2, null, group1, author, - contentType, timestamp, raw); - db.addMessage(txn, message2, true); - - // Mark one of the messages in the first group read - db.setReadFlag(txn, messageId, true); - - // There should be one unread message in each group - Map<GroupId, Integer> counts = db.getUnreadMessageCounts(txn); - assertEquals(2, counts.size()); - Integer count = counts.get(groupId); - assertNotNull(count); - assertEquals(1, count.intValue()); - count = counts.get(groupId1); - assertNotNull(count); - assertEquals(1, count.intValue()); - - // Mark the read message unread - db.setReadFlag(txn, messageId, false); - - // Mark the message in the second group read - db.setReadFlag(txn, messageId2, true); - - // There should be two unread messages in the first group, none in - // the second group - counts = db.getUnreadMessageCounts(txn); - assertEquals(1, counts.size()); - count = counts.get(groupId); - assertNotNull(count); - assertEquals(2, count.intValue()); - - db.commitTransaction(txn); - db.close(); - } - @Test public void testMultipleSubscriptionsAndUnsubscriptions() throws Exception { // Create some groups List<Group> groups = new ArrayList<Group>(); for (int i = 0; i < 100; i++) { GroupId id = new GroupId(TestUtils.getRandomId()); - String name = "Group " + i; - groups.add(new Group(id, name, new byte[GROUP_SALT_LENGTH])); + ClientId clientId = new ClientId(TestUtils.getRandomId()); + byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; + groups.add(new Group(id, clientId, descriptor)); } Database<Connection> db = open(false); @@ -1233,31 +902,34 @@ public class H2DatabaseTest extends BriarTestCase { db.setGroups(txn, contactId1, Collections.singletonList(group), 1); // The group should be available - assertEquals(Collections.emptyList(), db.getGroups(txn)); + assertEquals(Collections.emptyList(), db.getGroups(txn, clientId)); assertEquals(Collections.singletonList(group), - db.getAvailableGroups(txn)); + db.getAvailableGroups(txn, clientId)); // Subscribe to the group - it should no longer be available db.addGroup(txn, group); - assertEquals(Collections.singletonList(group), db.getGroups(txn)); - assertEquals(Collections.emptyList(), db.getAvailableGroups(txn)); + assertEquals(Collections.singletonList(group), + db.getGroups(txn, clientId)); + assertEquals(Collections.emptyList(), + db.getAvailableGroups(txn, clientId)); // Unsubscribe from the group - it should be available again db.removeGroup(txn, groupId); - assertEquals(Collections.emptyList(), db.getGroups(txn)); + assertEquals(Collections.emptyList(), db.getGroups(txn, clientId)); assertEquals(Collections.singletonList(group), - db.getAvailableGroups(txn)); + db.getAvailableGroups(txn, clientId)); // The first contact unsubscribes - it should still be available db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2); - assertEquals(Collections.emptyList(), db.getGroups(txn)); + assertEquals(Collections.emptyList(), db.getGroups(txn, clientId)); assertEquals(Collections.singletonList(group), - db.getAvailableGroups(txn)); + db.getAvailableGroups(txn, clientId)); // The second contact unsubscribes - it should no longer be available db.setGroups(txn, contactId1, Collections.<Group>emptyList(), 2); - assertEquals(Collections.emptyList(), db.getGroups(txn)); - assertEquals(Collections.emptyList(), db.getAvailableGroups(txn)); + assertEquals(Collections.emptyList(), db.getGroups(txn, clientId)); + assertEquals(Collections.emptyList(), + db.getAvailableGroups(txn, clientId)); db.commitTransaction(txn); db.close(); @@ -1288,41 +960,6 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } - @Test - public void testGetInboxMessageHeaders() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and an inbox group - no headers should be returned - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId)); - db.addGroup(txn, group); - db.setInboxGroup(txn, contactId, group); - assertEquals(Collections.emptyList(), - db.getInboxMessageHeaders(txn, contactId)); - - // Add a message to the inbox group - the header should be returned - db.addMessage(txn, message, true); - db.addStatus(txn, contactId, messageId, false, false); - Collection<MessageHeader> headers = - db.getInboxMessageHeaders(txn, contactId); - assertEquals(1, headers.size()); - MessageHeader header = headers.iterator().next(); - assertEquals(messageId, header.getId()); - assertNull(header.getParent()); - assertEquals(groupId, header.getGroupId()); - assertEquals(localAuthor, header.getAuthor()); - assertEquals(contentType, header.getContentType()); - assertEquals(timestamp, header.getTimestamp()); - assertEquals(true, header.isLocal()); - assertEquals(false, header.isRead()); - assertEquals(STORED, header.getStatus()); - assertFalse(header.isRead()); - - db.commitTransaction(txn); - db.close(); - } - @Test public void testOfferedMessages() throws Exception { Database<Connection> db = open(false); @@ -1392,6 +1029,116 @@ public class H2DatabaseTest extends BriarTestCase { db.close(); } + @Test + public void testMessageMetadata() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, true); + + // Attach some metadata to the message + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + db.mergeMessageMetadata(txn, messageId, metadata); + + // Retrieve the metadata for the message + Metadata retrieved = db.getMessageMetadata(txn, messageId); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + + // Retrieve the metadata for the group + Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageStatus() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact who subscribes to a group + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId)); + db.setGroups(txn, contactId, Collections.singletonList(group), 1); + + // Subscribe to the group and make it visible to the contact + db.addGroup(txn, group); + db.addVisibility(txn, contactId, groupId); + + // Add a message to the group + db.addMessage(txn, message, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The message should not be sent or seen + MessageStatus status = db.getMessageStatus(txn, contactId, messageId); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertFalse(status.isSent()); + assertFalse(status.isSeen()); + + // The same status should be returned when querying by group + Collection<MessageStatus> statuses = db.getMessageStatus(txn, + contactId, groupId); + assertEquals(1, statuses.size()); + status = statuses.iterator().next(); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertFalse(status.isSent()); + assertFalse(status.isSeen()); + + // Pretend the message was sent to the contact + db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE); + + // The message should be sent but not seen + status = db.getMessageStatus(txn, contactId, messageId); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertFalse(status.isSeen()); + + // The same status should be returned when querying by group + statuses = db.getMessageStatus(txn, contactId, groupId); + assertEquals(1, statuses.size()); + status = statuses.iterator().next(); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertFalse(status.isSeen()); + + // Pretend the message was acked by the contact + db.raiseSeenFlag(txn, contactId, messageId); + + // The message should be sent and seen + status = db.getMessageStatus(txn, contactId, messageId); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertTrue(status.isSeen()); + + // The same status should be returned when querying by group + statuses = db.getMessageStatus(txn, contactId, groupId); + assertEquals(1, statuses.size()); + status = statuses.iterator().next(); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertTrue(status.isSeen()); + + db.commitTransaction(txn); + db.close(); + } + @Test public void testExceptionHandling() throws Exception { Database<Connection> db = open(false); diff --git a/briar-tests/src/org/briarproject/sync/ConstantsTest.java b/briar-tests/src/org/briarproject/sync/ConstantsTest.java index 00149a8a012d92d23f63ec10acf89923567b90f2..6b92a026952a8de35dfa2b8421df2e98e665dc62 100644 --- a/briar-tests/src/org/briarproject/sync/ConstantsTest.java +++ b/briar-tests/src/org/briarproject/sync/ConstantsTest.java @@ -15,13 +15,19 @@ import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.KeyPair; import org.briarproject.api.crypto.PrivateKey; import org.briarproject.api.crypto.Signature; +import org.briarproject.api.forum.ForumConstants; +import org.briarproject.api.forum.ForumPost; +import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.messaging.MessagingConstants; +import org.briarproject.api.messaging.PrivateMessage; +import org.briarproject.api.messaging.PrivateMessageFactory; import org.briarproject.api.sync.Ack; +import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupFactory; -import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageFactory; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.Offer; import org.briarproject.api.sync.PacketWriter; @@ -33,6 +39,8 @@ import org.briarproject.crypto.CryptoModule; import org.briarproject.data.DataModule; import org.briarproject.db.DatabaseModule; import org.briarproject.event.EventModule; +import org.briarproject.forum.ForumModule; +import org.briarproject.messaging.MessagingModule; import org.junit.Test; import java.io.ByteArrayOutputStream; @@ -43,14 +51,14 @@ import java.util.Random; import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT; import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTY_LENGTH; import static org.briarproject.api.TransportPropertyConstants.MAX_TRANSPORT_ID_LENGTH; +import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH; import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_BODY_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_CONTENT_TYPE_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_GROUP_NAME_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_SUBSCRIPTIONS; +import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_SUBSCRIPTIONS; import static org.junit.Assert.assertTrue; public class ConstantsTest extends BriarTestCase { @@ -58,18 +66,21 @@ public class ConstantsTest extends BriarTestCase { private final CryptoComponent crypto; private final GroupFactory groupFactory; private final AuthorFactory authorFactory; - private final MessageFactory messageFactory; + private final PrivateMessageFactory privateMessageFactory; + private final ForumPostFactory forumPostFactory; private final PacketWriterFactory packetWriterFactory; public ConstantsTest() throws Exception { Injector i = Guice.createInjector(new TestDatabaseModule(), new TestLifecycleModule(), new TestSystemModule(), - new CryptoModule(), new DatabaseModule(), new EventModule(), - new SyncModule(), new DataModule()); + new CryptoModule(), new DatabaseModule(), new DataModule(), + new EventModule(), new ForumModule(), new MessagingModule(), + new SyncModule()); crypto = i.getInstance(CryptoComponent.class); groupFactory = i.getInstance(GroupFactory.class); authorFactory = i.getInstance(AuthorFactory.class); - messageFactory = i.getInstance(MessageFactory.class); + privateMessageFactory = i.getInstance(PrivateMessageFactory.class); + forumPostFactory = i.getInstance(ForumPostFactory.class); packetWriterFactory = i.getInstance(PacketWriterFactory.class); } @@ -100,14 +111,13 @@ public class ConstantsTest extends BriarTestCase { sig.initSign(keyPair.getPrivate()); sig.update(toBeSigned); byte[] signature = sig.sign(); - assertTrue("Length " + signature.length, - signature.length <= MAX_SIGNATURE_LENGTH); + assertTrue(signature.length <= MAX_SIGNATURE_LENGTH); } } @Test public void testMessageIdsFitIntoLargeAck() throws Exception { - testMessageIdsFitIntoAck(MAX_PAYLOAD_LENGTH); + testMessageIdsFitIntoAck(MAX_PACKET_PAYLOAD_LENGTH); } @Test @@ -116,36 +126,53 @@ public class ConstantsTest extends BriarTestCase { } @Test - public void testMessageFitsIntoPacket() throws Exception { + public void testPrivateMessageFitsIntoPacket() throws Exception { + // Create a maximum-length private message + GroupId groupId = new GroupId(TestUtils.getRandomId()); + long timestamp = Long.MAX_VALUE; MessageId parent = new MessageId(TestUtils.getRandomId()); - // Create a maximum-length group - String groupName = TestUtils.createRandomString(MAX_GROUP_NAME_LENGTH); - Group group = groupFactory.createGroup(groupName); + String contentType = TestUtils.createRandomString( + MessagingConstants.MAX_CONTENT_TYPE_LENGTH); + byte[] body = new byte[MAX_PRIVATE_MESSAGE_BODY_LENGTH]; + PrivateMessage message = privateMessageFactory.createPrivateMessage( + groupId, timestamp, parent, contentType, body); + // Check the size of the serialised message + int length = message.getMessage().getRaw().length; + assertTrue(length > UniqueId.LENGTH + 8 + UniqueId.LENGTH + + MessagingConstants.MAX_CONTENT_TYPE_LENGTH + + MAX_PRIVATE_MESSAGE_BODY_LENGTH); + assertTrue(length <= MAX_PACKET_PAYLOAD_LENGTH); + } + + @Test + public void testForumPostFitsIntoPacket() throws Exception { // Create a maximum-length author - String authorName = - TestUtils.createRandomString(MAX_AUTHOR_NAME_LENGTH); + String authorName = TestUtils.createRandomString( + MAX_AUTHOR_NAME_LENGTH); byte[] authorPublic = new byte[MAX_PUBLIC_KEY_LENGTH]; Author author = authorFactory.createAuthor(authorName, authorPublic); - // Create a maximum-length message - PrivateKey privateKey = crypto.generateSignatureKeyPair().getPrivate(); - String contentType = - TestUtils.createRandomString(MAX_CONTENT_TYPE_LENGTH); + // Create a maximum-length forum post + GroupId groupId = new GroupId(TestUtils.getRandomId()); long timestamp = Long.MAX_VALUE; - byte[] body = new byte[MAX_BODY_LENGTH]; - Message message = messageFactory.createPseudonymousMessage(parent, - group, author, privateKey, contentType, timestamp, body); + MessageId parent = new MessageId(TestUtils.getRandomId()); + String contentType = TestUtils.createRandomString( + ForumConstants.MAX_CONTENT_TYPE_LENGTH); + byte[] body = new byte[MAX_FORUM_POST_BODY_LENGTH]; + PrivateKey privateKey = crypto.generateSignatureKeyPair().getPrivate(); + ForumPost post = forumPostFactory.createPseudonymousPost(groupId, + timestamp, parent, author, contentType, body, privateKey); // Check the size of the serialised message - int length = message.getSerialised().length; - assertTrue(length > UniqueId.LENGTH + MAX_GROUP_NAME_LENGTH - + MAX_PUBLIC_KEY_LENGTH + MAX_AUTHOR_NAME_LENGTH - + MAX_PUBLIC_KEY_LENGTH + MAX_CONTENT_TYPE_LENGTH - + MAX_BODY_LENGTH); - assertTrue(length <= MAX_PAYLOAD_LENGTH); + int length = post.getMessage().getRaw().length; + assertTrue(length > UniqueId.LENGTH + 8 + UniqueId.LENGTH + + MAX_AUTHOR_NAME_LENGTH + MAX_PUBLIC_KEY_LENGTH + + ForumConstants.MAX_CONTENT_TYPE_LENGTH + + MAX_FORUM_POST_BODY_LENGTH); + assertTrue(length <= MAX_PACKET_PAYLOAD_LENGTH); } @Test public void testMessageIdsFitIntoLargeOffer() throws Exception { - testMessageIdsFitIntoOffer(MAX_PAYLOAD_LENGTH); + testMessageIdsFitIntoOffer(MAX_PACKET_PAYLOAD_LENGTH); } @Test @@ -155,7 +182,7 @@ public class ConstantsTest extends BriarTestCase { @Test public void testMessageIdsFitIntoLargeRequest() throws Exception { - testMessageIdsFitIntoRequest(MAX_PAYLOAD_LENGTH); + testMessageIdsFitIntoRequest(MAX_PACKET_PAYLOAD_LENGTH); } @Test @@ -181,16 +208,19 @@ public class ConstantsTest extends BriarTestCase { PacketWriter writer = packetWriterFactory.createPacketWriter(out); writer.writeTransportUpdate(u); // Check the size of the serialised transport update - assertTrue(out.size() <= MAX_PAYLOAD_LENGTH); + assertTrue(out.size() <= MAX_PACKET_PAYLOAD_LENGTH); } @Test public void testGroupsFitIntoSubscriptionUpdate() throws Exception { // Create the maximum number of maximum-length groups + Random random = new Random(); + ClientId clientId = new ClientId(TestUtils.getRandomId()); Collection<Group> groups = new ArrayList<Group>(); for (int i = 0; i < MAX_SUBSCRIPTIONS; i++) { - String name = TestUtils.createRandomString(MAX_GROUP_NAME_LENGTH); - groups.add(groupFactory.createGroup(name)); + byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; + random.nextBytes(descriptor); + groups.add(groupFactory.createGroup(clientId, descriptor)); } // Create a maximum-length subscription update SubscriptionUpdate u = new SubscriptionUpdate(groups, Long.MAX_VALUE); @@ -199,7 +229,7 @@ public class ConstantsTest extends BriarTestCase { PacketWriter writer = packetWriterFactory.createPacketWriter(out); writer.writeSubscriptionUpdate(u); // Check the size of the serialised subscription update - assertTrue(out.size() <= MAX_PAYLOAD_LENGTH); + assertTrue(out.size() <= MAX_PACKET_PAYLOAD_LENGTH); } private void testMessageIdsFitIntoAck(int length) throws Exception { diff --git a/briar-tests/src/org/briarproject/sync/ConsumersTest.java b/briar-tests/src/org/briarproject/sync/ConsumersTest.java deleted file mode 100644 index d19e0223da35b5b80df3c50714cfc08b5aede4fd..0000000000000000000000000000000000000000 --- a/briar-tests/src/org/briarproject/sync/ConsumersTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.briarproject.sync; - -import org.briarproject.BriarTestCase; -import org.briarproject.api.FormatException; -import org.briarproject.api.crypto.MessageDigest; -import org.junit.Test; - -import java.security.GeneralSecurityException; -import java.util.Random; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class ConsumersTest extends BriarTestCase { - - @Test - public void testDigestingConsumer() throws Exception { - byte[] data = new byte[1234]; - // Generate some random data and digest it - new Random().nextBytes(data); - MessageDigest messageDigest = new TestMessageDigest(); - messageDigest.update(data); - byte[] dig = messageDigest.digest(); - // Check that feeding a DigestingConsumer generates the same digest - DigestingConsumer dc = new DigestingConsumer(messageDigest); - dc.write(data[0]); - dc.write(data, 1, data.length - 2); - dc.write(data[data.length - 1]); - byte[] dig1 = messageDigest.digest(); - assertArrayEquals(dig, dig1); - } - - @Test(expected = FormatException.class) - public void testCountingConsumer() throws Exception { - byte[] data = new byte[1234]; - CountingConsumer cc = new CountingConsumer(data.length); - cc.write(data[0]); - cc.write(data, 1, data.length - 2); - cc.write(data[data.length - 1]); - assertEquals(data.length, cc.getCount()); - cc.write((byte) 0); - } - - @Test - public void testCopyingConsumer() throws Exception { - byte[] data = new byte[1234]; - new Random().nextBytes(data); - // Check that a CopyingConsumer creates a faithful copy - CopyingConsumer cc = new CopyingConsumer(); - cc.write(data[0]); - cc.write(data, 1, data.length - 2); - cc.write(data[data.length - 1]); - assertArrayEquals(data, cc.getCopy()); - } - - private static class TestMessageDigest implements MessageDigest { - - private final java.security.MessageDigest delegate; - - private TestMessageDigest() throws GeneralSecurityException { - delegate = java.security.MessageDigest.getInstance("SHA-256"); - } - - public byte[] digest() { - return delegate.digest(); - } - - public byte[] digest(byte[] input) { - return delegate.digest(input); - } - - public int digest(byte[] buf, int offset, int len) { - byte[] digest = digest(); - len = Math.min(len, digest.length); - System.arraycopy(digest, 0, buf, offset, len); - return len; - } - - public int getDigestLength() { - return delegate.getDigestLength(); - } - - public void reset() { - delegate.reset(); - } - - public void update(byte input) { - delegate.update(input); - } - - public void update(byte[] input) { - delegate.update(input); - } - - public void update(byte[] input, int offset, int len) { - delegate.update(input, offset, len); - } - } -} diff --git a/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java b/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java index 194fe2006663ff6cffe612e2f979204ee93c6f16..7224f5b6e28263f03d66a84c10d317af5d26531d 100644 --- a/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java +++ b/briar-tests/src/org/briarproject/sync/PacketReaderImplTest.java @@ -1,50 +1,29 @@ package org.briarproject.sync; -import com.google.inject.Guice; -import com.google.inject.Injector; - import org.briarproject.BriarTestCase; import org.briarproject.TestUtils; import org.briarproject.api.FormatException; -import org.briarproject.api.data.BdfReaderFactory; -import org.briarproject.api.data.BdfWriter; -import org.briarproject.api.data.BdfWriterFactory; -import org.briarproject.data.DataModule; +import org.briarproject.api.UniqueId; import org.briarproject.util.ByteUtils; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import static org.briarproject.api.data.DataConstants.LIST_END_LENGTH; -import static org.briarproject.api.data.DataConstants.UNIQUE_ID_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.HEADER_LENGTH; -import static org.briarproject.api.sync.MessagingConstants.MAX_PAYLOAD_LENGTH; import static org.briarproject.api.sync.PacketTypes.ACK; import static org.briarproject.api.sync.PacketTypes.OFFER; import static org.briarproject.api.sync.PacketTypes.REQUEST; +import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH; +import static org.briarproject.api.sync.SyncConstants.PACKET_HEADER_LENGTH; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; public class PacketReaderImplTest extends BriarTestCase { - // FIXME: This is an integration test, not a unit test - - private final BdfReaderFactory bdfReaderFactory; - private final BdfWriterFactory bdfWriterFactory; - - public PacketReaderImplTest() throws Exception { - Injector i = Guice.createInjector(new DataModule()); - bdfReaderFactory = i.getInstance(BdfReaderFactory.class); - bdfWriterFactory = i.getInstance(BdfWriterFactory.class); - } - @Test(expected = FormatException.class) public void testFormatExceptionIfAckIsTooLarge() throws Exception { byte[] b = createAck(true); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readAck(); } @@ -52,8 +31,7 @@ public class PacketReaderImplTest extends BriarTestCase { public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception { byte[] b = createAck(false); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readAck(); } @@ -61,8 +39,7 @@ public class PacketReaderImplTest extends BriarTestCase { public void testEmptyAck() throws Exception { byte[] b = createEmptyAck(); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readAck(); } @@ -70,8 +47,7 @@ public class PacketReaderImplTest extends BriarTestCase { public void testFormatExceptionIfOfferIsTooLarge() throws Exception { byte[] b = createOffer(true); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readOffer(); } @@ -79,10 +55,7 @@ public class PacketReaderImplTest extends BriarTestCase { public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception { byte[] b = createOffer(false); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl - reader = new PacketReaderImpl( - bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readOffer(); } @@ -90,8 +63,7 @@ public class PacketReaderImplTest extends BriarTestCase { public void testEmptyOffer() throws Exception { byte[] b = createEmptyOffer(); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readOffer(); } @@ -99,8 +71,7 @@ public class PacketReaderImplTest extends BriarTestCase { public void testFormatExceptionIfRequestIsTooLarge() throws Exception { byte[] b = createRequest(true); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readRequest(); } @@ -108,8 +79,7 @@ public class PacketReaderImplTest extends BriarTestCase { public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception { byte[] b = createRequest(false); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readRequest(); } @@ -117,110 +87,76 @@ public class PacketReaderImplTest extends BriarTestCase { public void testEmptyRequest() throws Exception { byte[] b = createEmptyRequest(); ByteArrayInputStream in = new ByteArrayInputStream(b); - PacketReaderImpl reader = new PacketReaderImpl(bdfReaderFactory, null, - null, in); + PacketReaderImpl reader = new PacketReaderImpl(null, null, null, in); reader.readRequest(); } private byte[] createAck(boolean tooBig) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(new byte[HEADER_LENGTH]); - BdfWriter w = bdfWriterFactory.createWriter(out); - w.writeListStart(); - w.writeListStart(); - while (out.size() + UNIQUE_ID_LENGTH + LIST_END_LENGTH * 2 - < HEADER_LENGTH + MAX_PAYLOAD_LENGTH) { - w.writeRaw(TestUtils.getRandomId()); + out.write(new byte[PACKET_HEADER_LENGTH]); + while (out.size() + UniqueId.LENGTH <= PACKET_HEADER_LENGTH + + MAX_PACKET_PAYLOAD_LENGTH) { + out.write(TestUtils.getRandomId()); } - if (tooBig) w.writeRaw(TestUtils.getRandomId()); - w.writeListEnd(); - w.writeListEnd(); - assertEquals(tooBig, out.size() > HEADER_LENGTH + MAX_PAYLOAD_LENGTH); + if (tooBig) out.write(TestUtils.getRandomId()); + assertEquals(tooBig, out.size() > PACKET_HEADER_LENGTH + + MAX_PACKET_PAYLOAD_LENGTH); byte[] packet = out.toByteArray(); packet[1] = ACK; - ByteUtils.writeUint16(packet.length - HEADER_LENGTH, packet, 2); + ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2); return packet; } private byte[] createEmptyAck() throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(new byte[HEADER_LENGTH]); - BdfWriter w = bdfWriterFactory.createWriter(out); - w.writeListStart(); - w.writeListStart(); - w.writeListEnd(); - w.writeListEnd(); - byte[] packet = out.toByteArray(); + byte[] packet = new byte[PACKET_HEADER_LENGTH]; packet[1] = ACK; - ByteUtils.writeUint16(packet.length - HEADER_LENGTH, packet, 2); + ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2); return packet; } private byte[] createOffer(boolean tooBig) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(new byte[HEADER_LENGTH]); - BdfWriter w = bdfWriterFactory.createWriter(out); - w.writeListStart(); - w.writeListStart(); - while (out.size() + UNIQUE_ID_LENGTH + LIST_END_LENGTH * 2 - < HEADER_LENGTH + MAX_PAYLOAD_LENGTH) { - w.writeRaw(TestUtils.getRandomId()); + out.write(new byte[PACKET_HEADER_LENGTH]); + while (out.size() + UniqueId.LENGTH <= PACKET_HEADER_LENGTH + + MAX_PACKET_PAYLOAD_LENGTH) { + out.write(TestUtils.getRandomId()); } - if (tooBig) w.writeRaw(TestUtils.getRandomId()); - w.writeListEnd(); - w.writeListEnd(); - assertEquals(tooBig, out.size() > HEADER_LENGTH + MAX_PAYLOAD_LENGTH); + if (tooBig) out.write(TestUtils.getRandomId()); + assertEquals(tooBig, out.size() > PACKET_HEADER_LENGTH + + MAX_PACKET_PAYLOAD_LENGTH); byte[] packet = out.toByteArray(); packet[1] = OFFER; - ByteUtils.writeUint16(packet.length - HEADER_LENGTH, packet, 2); + ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2); return packet; } private byte[] createEmptyOffer() throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(new byte[HEADER_LENGTH]); - BdfWriter w = bdfWriterFactory.createWriter(out); - w.writeListStart(); - w.writeListStart(); - w.writeListEnd(); - w.writeListEnd(); - byte[] packet = out.toByteArray(); + byte[] packet = new byte[PACKET_HEADER_LENGTH]; packet[1] = OFFER; - ByteUtils.writeUint16(packet.length - HEADER_LENGTH, packet, 2); + ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2); return packet; } private byte[] createRequest(boolean tooBig) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(new byte[HEADER_LENGTH]); - BdfWriter w = bdfWriterFactory.createWriter(out); - w.writeListStart(); - w.writeListStart(); - while (out.size() + UNIQUE_ID_LENGTH + LIST_END_LENGTH * 2 - < HEADER_LENGTH + MAX_PAYLOAD_LENGTH) { - w.writeRaw(TestUtils.getRandomId()); + out.write(new byte[PACKET_HEADER_LENGTH]); + while (out.size() + UniqueId.LENGTH <= PACKET_HEADER_LENGTH + + MAX_PACKET_PAYLOAD_LENGTH) { + out.write(TestUtils.getRandomId()); } - if (tooBig) w.writeRaw(TestUtils.getRandomId()); - w.writeListEnd(); - w.writeListEnd(); - assertEquals(tooBig, out.size() > HEADER_LENGTH + MAX_PAYLOAD_LENGTH); + if (tooBig) out.write(TestUtils.getRandomId()); + assertEquals(tooBig, out.size() > PACKET_HEADER_LENGTH + + MAX_PACKET_PAYLOAD_LENGTH); byte[] packet = out.toByteArray(); packet[1] = REQUEST; - ByteUtils.writeUint16(packet.length - HEADER_LENGTH, packet, 2); + ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2); return packet; } private byte[] createEmptyRequest() throws Exception { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(new byte[HEADER_LENGTH]); - BdfWriter w = bdfWriterFactory.createWriter(out); - w.writeListStart(); - w.writeListStart(); - w.writeListEnd(); - w.writeListEnd(); - byte[] packet = out.toByteArray(); + byte[] packet = new byte[PACKET_HEADER_LENGTH]; packet[1] = REQUEST; - ByteUtils.writeUint16(packet.length - HEADER_LENGTH, packet, 2); + ByteUtils.writeUint16(packet.length - PACKET_HEADER_LENGTH, packet, 2); return packet; } } diff --git a/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java b/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java index 88ae60a383c9c83b4ccfce212c127b58d4269444..3a7be3b74b4254f3299a1bbb75a2cd878f3fdf02 100644 --- a/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java +++ b/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java @@ -22,16 +22,14 @@ import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.messaging.MessagingManager; -import org.briarproject.api.messaging.PrivateConversation; +import org.briarproject.api.messaging.PrivateMessage; import org.briarproject.api.messaging.PrivateMessageFactory; import org.briarproject.api.sync.GroupId; -import org.briarproject.api.sync.Message; -import org.briarproject.api.sync.MessageVerifier; -import org.briarproject.api.sync.MessagingSession; import org.briarproject.api.sync.PacketReader; import org.briarproject.api.sync.PacketReaderFactory; import org.briarproject.api.sync.PacketWriter; import org.briarproject.api.sync.PacketWriterFactory; +import org.briarproject.api.sync.SyncSession; import org.briarproject.api.transport.KeyManager; import org.briarproject.api.transport.StreamContext; import org.briarproject.api.transport.StreamReaderFactory; @@ -73,6 +71,8 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { private final TransportId transportId = new TransportId("id"); private final SecretKey master = TestUtils.createSecretKey(); private final long timestamp = System.currentTimeMillis(); + private final AuthorId aliceId = new AuthorId(TestUtils.getRandomId()); + private final AuthorId bobId = new AuthorId(TestUtils.getRandomId()); private Injector alice, bob; @@ -106,14 +106,12 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { KeyManager keyManager = alice.getInstance(KeyManager.class); keyManager.start(); // Add an identity for Alice - AuthorId aliceId = new AuthorId(TestUtils.getRandomId()); LocalAuthor aliceAuthor = new LocalAuthor(aliceId, "Alice", - new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100], 1234); + new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100], timestamp); IdentityManager identityManager = alice.getInstance(IdentityManager.class); identityManager.addLocalAuthor(aliceAuthor); // Add Bob as a contact - AuthorId bobId = new AuthorId(TestUtils.getRandomId()); Author bobAuthor = new Author(bobId, "Bob", new byte[MAX_PUBLIC_KEY_LENGTH]); ContactManager contactManager = alice.getInstance(ContactManager.class); @@ -121,19 +119,17 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { // Create the private conversation MessagingManager messagingManager = alice.getInstance(MessagingManager.class); - messagingManager.addContact(contactId, master); + messagingManager.addContact(contactId); // Derive and store the transport keys keyManager.addContact(contactId, Collections.singletonList(transportId), master, timestamp, true); // Send Bob a message - byte[] body = "Hi Bob!".getBytes("UTF-8"); - PrivateMessageFactory messageFactory = + PrivateMessageFactory privateMessageFactory = alice.getInstance(PrivateMessageFactory.class); GroupId groupId = messagingManager.getConversationId(contactId); - PrivateConversation conversation = - messagingManager.getConversation(groupId); - Message message = messageFactory.createPrivateMessage(null, - conversation, "text/plain", timestamp, body); + byte[] body = "Hi Bob!".getBytes("UTF-8"); + PrivateMessage message = privateMessageFactory.createPrivateMessage( + groupId, timestamp, null, "text/plain", body); messagingManager.addLocalMessage(message); // Get a stream context StreamContext ctx = keyManager.getStreamContext(contactId, transportId); @@ -150,7 +146,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { alice.getInstance(PacketWriterFactory.class); PacketWriter packetWriter = packetWriterFactory.createPacketWriter( streamWriter); - MessagingSession session = new SimplexOutgoingSession(db, + SyncSession session = new SimplexOutgoingSession(db, new ImmediateExecutor(), eventBus, contactId, transportId, MAX_LATENCY, packetWriter); // Write whatever needs to be written @@ -173,14 +169,12 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { KeyManager keyManager = bob.getInstance(KeyManager.class); keyManager.start(); // Add an identity for Bob - AuthorId bobId = new AuthorId(TestUtils.getRandomId()); LocalAuthor bobAuthor = new LocalAuthor(bobId, "Bob", - new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100], 1234); + new byte[MAX_PUBLIC_KEY_LENGTH], new byte[100], timestamp); IdentityManager identityManager = bob.getInstance(IdentityManager.class); identityManager.addLocalAuthor(bobAuthor); // Add Alice as a contact - AuthorId aliceId = new AuthorId(TestUtils.getRandomId()); Author aliceAuthor = new Author(aliceId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); ContactManager contactManager = bob.getInstance(ContactManager.class); @@ -188,7 +182,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { // Create the private conversation MessagingManager messagingManager = bob.getInstance(MessagingManager.class); - messagingManager.addContact(contactId, master); + messagingManager.addContact(contactId); // Derive and store the transport keys keyManager.addContact(contactId, Collections.singletonList(transportId), master, timestamp, false); @@ -209,15 +203,13 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase { streamReaderFactory.createStreamReader(in, ctx); // Create an incoming sync session EventBus eventBus = bob.getInstance(EventBus.class); - MessageVerifier messageVerifier = - bob.getInstance(MessageVerifier.class); PacketReaderFactory packetReaderFactory = bob.getInstance(PacketReaderFactory.class); PacketReader packetReader = packetReaderFactory.createPacketReader( streamReader); - MessagingSession session = new IncomingSession(db, - new ImmediateExecutor(), new ImmediateExecutor(), eventBus, - messageVerifier, contactId, transportId, packetReader); + SyncSession session = new IncomingSession(db, + new ImmediateExecutor(), eventBus, contactId, transportId, + packetReader); // No messages should have been added yet assertFalse(listener.messageAdded); // Read whatever needs to be read diff --git a/briar-tests/src/org/briarproject/util/ByteUtilsTest.java b/briar-tests/src/org/briarproject/util/ByteUtilsTest.java index 78843631dcde79bcbc28fb2bb66d8d3f440abae1..11919c5fc60fcd1d270c534b8ea5849d70260584 100644 --- a/briar-tests/src/org/briarproject/util/ByteUtilsTest.java +++ b/briar-tests/src/org/briarproject/util/ByteUtilsTest.java @@ -12,11 +12,13 @@ public class ByteUtilsTest extends BriarTestCase { @Test public void testReadUint16() { - byte[] b = StringUtils.fromHexString("000000"); + byte[] b = StringUtils.fromHexString("00000000"); assertEquals(0, ByteUtils.readUint16(b, 1)); - b = StringUtils.fromHexString("000001"); + b = StringUtils.fromHexString("00000100"); assertEquals(1, ByteUtils.readUint16(b, 1)); - b = StringUtils.fromHexString("00FFFF"); + b = StringUtils.fromHexString("007FFF00"); + assertEquals(Short.MAX_VALUE, ByteUtils.readUint16(b, 1)); + b = StringUtils.fromHexString("00FFFF00"); assertEquals(65535, ByteUtils.readUint16(b, 1)); } @@ -32,11 +34,13 @@ public class ByteUtilsTest extends BriarTestCase { @Test public void testReadUint32() { - byte[] b = StringUtils.fromHexString("0000000000"); + byte[] b = StringUtils.fromHexString("000000000000"); assertEquals(0, ByteUtils.readUint32(b, 1)); - b = StringUtils.fromHexString("0000000001"); + b = StringUtils.fromHexString("000000000100"); assertEquals(1, ByteUtils.readUint32(b, 1)); - b = StringUtils.fromHexString("00FFFFFFFF"); + b = StringUtils.fromHexString("007FFFFFFF00"); + assertEquals(Integer.MAX_VALUE, ByteUtils.readUint32(b, 1)); + b = StringUtils.fromHexString("00FFFFFFFF00"); assertEquals(4294967295L, ByteUtils.readUint32(b, 1)); } @@ -50,6 +54,30 @@ public class ByteUtilsTest extends BriarTestCase { ByteUtils.readUint32(new byte[4], 1); } + @Test + public void testReadUint64() { + byte[] b = StringUtils.fromHexString("00000000000000000000"); + assertEquals(0L, ByteUtils.readUint64(b, 1)); + b = StringUtils.fromHexString("00000000000000000100"); + assertEquals(1L, ByteUtils.readUint64(b, 1)); + b = StringUtils.fromHexString("007FFFFFFFFFFFFFFF00"); + assertEquals(Long.MAX_VALUE, ByteUtils.readUint64(b, 1)); + b = StringUtils.fromHexString("00800000000000000000"); + assertEquals(Long.MIN_VALUE, ByteUtils.readUint64(b, 1)); + b = StringUtils.fromHexString("00FFFFFFFFFFFFFFFF00"); + assertEquals(-1L, ByteUtils.readUint64(b, 1)); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadUint64ValidatesArguments1() { + ByteUtils.readUint64(new byte[7], 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testReadUint64ValidatesArguments2() { + ByteUtils.readUint64(new byte[8], 1); + } + @Test public void testWriteUint16() { byte[] b = new byte[4];