diff --git a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java index 38b8ce59daf3e4209ab13357bd7b575a8826b71b..1668c92eda187bb02e03c871878c5877da762519 100644 --- a/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java +++ b/briar-android/src/net/sf/briar/android/groups/WriteGroupMessageActivity.java @@ -9,7 +9,10 @@ import static java.util.logging.Level.WARNING; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -53,7 +56,6 @@ implements OnClickListener, OnItemSelectedListener { new BriarServiceConnection(); @Inject private BundleEncrypter bundleEncrypter; - private boolean restricted = false; private GroupNameSpinnerAdapter adapter = null; private Spinner spinner = null; private ImageButton sendButton = null; @@ -63,6 +65,7 @@ implements OnClickListener, OnItemSelectedListener { @Inject private volatile DatabaseComponent db; @Inject @DatabaseExecutor private volatile Executor dbExecutor; @Inject private volatile MessageFactory messageFactory; + private volatile boolean restricted = false; private volatile Group group = null; private volatile GroupId groupId = null; private volatile MessageId parentId = null; @@ -131,7 +134,14 @@ implements OnClickListener, OnItemSelectedListener { public void run() { try { serviceConnection.waitForStartup(); - updateGroupList(db.getSubscriptions()); + List<Group> postable = new ArrayList<Group>(); + if(restricted) { + postable.addAll(db.getLocalGroups()); + } else { + for(Group g : db.getSubscriptions()) + if(!g.isRestricted()) postable.add(g); + } + updateGroupList(Collections.unmodifiableList(postable)); } catch(DbException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -147,7 +157,6 @@ implements OnClickListener, OnItemSelectedListener { runOnUiThread(new Runnable() { public void run() { for(Group g : groups) { - if(g.isRestricted() != restricted) continue; if(g.getId().equals(groupId)) { group = g; spinner.setSelection(adapter.getCount()); diff --git a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java index add29d7abd1e926fe08efc02b5f63049b6b5cfa2..ba5f1848e9bb7fe29144e0f13c37e83d6b8a5203 100644 --- a/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java +++ b/briar-api/src/net/sf/briar/api/db/DatabaseComponent.java @@ -14,6 +14,8 @@ import net.sf.briar.api.messaging.Ack; import net.sf.briar.api.messaging.AuthorId; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.LocalAuthor; +import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; import net.sf.briar.api.messaging.Offer; @@ -51,18 +53,26 @@ public interface DatabaseComponent { void removeListener(DatabaseListener d); /** - * Adds a contact with the given name to the database and returns an ID for - * the contact. + * Stores a contact with the given name and returns an ID for the contact. */ ContactId addContact(String name) throws DbException; - /** Adds an endpoint to the database. */ + /** Stores an endpoint. */ void addEndpoint(Endpoint ep) throws DbException; - /** Adds a locally generated group message to the database. */ + /** Stores a pseudonym that the user can use to sign messages. */ + void addLocalAuthor(LocalAuthor a) throws DbException; + + /** + * Stores a restricted group to which the user can post messages. Storing + * a group does not create a subscription to it. + */ + void addLocalGroup(LocalGroup g) throws DbException; + + /** Stores a locally generated group message. */ void addLocalGroupMessage(Message m) throws DbException; - /** Adds a locally generated private message to the database. */ + /** Stores a locally generated private message. */ void addLocalPrivateMessage(Message m, ContactId c) throws DbException; /** @@ -72,13 +82,13 @@ public interface DatabaseComponent { void addSecrets(Collection<TemporarySecret> secrets) throws DbException; /** - * Adds a transport to the database and returns true if the transport was - * not previously in the database. + * Stores a transport and returns true if the transport was not previously + * in the database. */ boolean addTransport(TransportId t) throws DbException; /** - * Generates an acknowledgement for the given contact. Returns null if + * Generates an acknowledgement for the given contact, or returns null if * there are no messages to acknowledge. */ Ack generateAck(ContactId c, int maxMessages) throws DbException; @@ -106,14 +116,14 @@ public interface DatabaseComponent { throws DbException; /** - * Generates an offer for the given contact. Returns null if there are no - * messages to offer. + * Generates an offer for the given contact, or returns null if there are + * no messages to offer. */ Offer generateOffer(ContactId c, int maxMessages) throws DbException; /** - * Generates a retention ack for the given contact. Returns null if no ack - * is due. + * Generates a retention ack for the given contact, or returns null if no + * ack is due. */ RetentionAck generateRetentionAck(ContactId c) throws DbException; @@ -126,8 +136,8 @@ public interface DatabaseComponent { throws DbException; /** - * Generates a subscription ack for the given contact. Returns null if no - * ack is due. + * Generates a subscription ack for the given contact, or returns null if + * no ack is due. */ SubscriptionAck generateSubscriptionAck(ContactId c) throws DbException; @@ -140,8 +150,8 @@ public interface DatabaseComponent { throws DbException; /** - * Generates a batch of transport acks for the given contact. Returns null - * if no acks are due. + * Generates a batch of transport acks for the given contact, or returns + * null if no acks are due. */ Collection<TransportAck> generateTransportAcks(ContactId c) throws DbException; @@ -166,6 +176,12 @@ public interface DatabaseComponent { /** Returns the group with the given ID, if the user subscribes to it. */ Group getGroup(GroupId g) throws DbException; + /** Returns all pseudonyms that the user can use to sign messages. */ + Collection<LocalAuthor> getLocalAuthors() throws DbException; + + /** Returns all restricted groups to which the user can post messages. */ + Collection<LocalGroup> getLocalGroups() throws DbException; + /** Returns the local transport properties for the given transport. */ TransportProperties getLocalProperties(TransportId t) throws DbException; @@ -219,8 +235,8 @@ public interface DatabaseComponent { boolean hasSendableMessages(ContactId c) throws DbException; /** - * Increments the outgoing connection counter for the given contact - * transport in the given rotation period and returns the old value. + * Increments the outgoing connection counter for the given endpoint + * in the given rotation period and returns the old value of the counter. */ long incrementConnectionCounter(ContactId c, TransportId t, long period) throws DbException; diff --git a/briar-api/src/net/sf/briar/api/messaging/LocalAuthor.java b/briar-api/src/net/sf/briar/api/messaging/LocalAuthor.java new file mode 100644 index 0000000000000000000000000000000000000000..d60f44b4559f6c51374ac2fd8c04ad87c1f4b36c --- /dev/null +++ b/briar-api/src/net/sf/briar/api/messaging/LocalAuthor.java @@ -0,0 +1,18 @@ +package net.sf.briar.api.messaging; + +/** A pseudonym that the user can use to sign {@link Message}s. */ +public class LocalAuthor extends Author { + + private final byte[] privateKey; + + public LocalAuthor(AuthorId id, String name, byte[] publicKey, + byte[] privateKey) { + super(id, name, publicKey); + this.privateKey = privateKey; + } + + /** Returns the private key that is used to sign messages. */ + public byte[] getPrivateKey() { + return privateKey; + } +} diff --git a/briar-api/src/net/sf/briar/api/messaging/LocalGroup.java b/briar-api/src/net/sf/briar/api/messaging/LocalGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..27bdca0fad7f04103c45842f40bfe42e0d369311 --- /dev/null +++ b/briar-api/src/net/sf/briar/api/messaging/LocalGroup.java @@ -0,0 +1,18 @@ +package net.sf.briar.api.messaging; + +/** A restricted group to which the user can post messages. */ +public class LocalGroup extends Group { + + private final byte[] privateKey; + + public LocalGroup(GroupId id, String name, byte[] publicKey, + byte[] privateKey) { + super(id, name, publicKey); + this.privateKey = privateKey; + } + + /** Returns the private key that is used to sign messages. */ + public byte[] getPrivateKey() { + return privateKey; + } +} diff --git a/briar-core/src/net/sf/briar/db/Database.java b/briar-core/src/net/sf/briar/db/Database.java index 5ed55973f37bc39d872c67a7e11b97a22a010c34..f3cd30c36c35001a1ceceb2a8d3016d82fa4962f 100644 --- a/briar-core/src/net/sf/briar/db/Database.java +++ b/briar-core/src/net/sf/briar/db/Database.java @@ -15,6 +15,8 @@ import net.sf.briar.api.db.PrivateMessageHeader; import net.sf.briar.api.messaging.AuthorId; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.LocalAuthor; +import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; import net.sf.briar.api.messaging.RetentionAck; @@ -40,6 +42,7 @@ import net.sf.briar.api.transport.TemporarySecret; * deadlock, locks must be acquired in the following (alphabetical) order: * <ul> * <li> contact + * <li> identity * <li> message * <li> rating * <li> retention @@ -102,6 +105,21 @@ interface Database<T> { */ boolean addGroupMessage(T txn, Message m) throws DbException; + /** + * Stores a pseudonym that the user can use to sign messages. + * <p> + * Locking: identity write. + */ + void addLocalAuthor(T txn, LocalAuthor a) throws DbException; + + /** + * Stores a restricted group to which the user can post messages. Storing + * a group does not create a subscription to it. + * <p> + * Locking: identity write. + */ + void addLocalGroup(T txn, LocalGroup g) throws DbException; + /** * Records a received message as needing to be acknowledged. * <p> @@ -259,6 +277,20 @@ interface Database<T> { */ long getLastConnected(T txn, ContactId c) throws DbException; + /** + * Returns all pseudonyms that the user can use to sign messages. + * <p> + * Locking: identity read. + */ + Collection<LocalAuthor> getLocalAuthors(T txn) throws DbException; + + /** + * Returns all restricted groups to which the user can post messages. + * <p> + * Locking: identity read. + */ + Collection<LocalGroup> getLocalGroups(T txn) throws DbException; + /** * Returns the local transport properties for the given transport. * <p> diff --git a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java index f5184749ca9db506e8ab2b9fed94af00a3dbd5f5..a4edf9750aec385dc6c6580d00e8dfd5925cf664 100644 --- a/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java +++ b/briar-core/src/net/sf/briar/db/DatabaseComponentImpl.java @@ -61,6 +61,8 @@ import net.sf.briar.api.messaging.Author; import net.sf.briar.api.messaging.AuthorId; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.LocalAuthor; +import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; import net.sf.briar.api.messaging.Offer; @@ -97,6 +99,8 @@ DatabaseCleaner.Callback { private final ReentrantReadWriteLock contactLock = new ReentrantReadWriteLock(true); + private final ReentrantReadWriteLock identityLock = + new ReentrantReadWriteLock(true); private final ReentrantReadWriteLock messageLock = new ReentrantReadWriteLock(true); private final ReentrantReadWriteLock ratingLock = @@ -426,6 +430,38 @@ DatabaseCleaner.Callback { return true; } + public void addLocalAuthor(LocalAuthor a) throws DbException { + identityLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + db.addLocalAuthor(txn, a); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + identityLock.writeLock().unlock(); + } + } + + public void addLocalGroup(LocalGroup g) throws DbException { + identityLock.writeLock().lock(); + try { + T txn = db.startTransaction(); + try { + db.addLocalGroup(txn, g); + db.commitTransaction(txn); + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + identityLock.writeLock().unlock(); + } + } + public void addSecrets(Collection<TemporarySecret> secrets) throws DbException { contactLock.readLock().lock(); @@ -911,6 +947,40 @@ DatabaseCleaner.Callback { } } + public Collection<LocalAuthor> getLocalAuthors() throws DbException { + identityLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + Collection<LocalAuthor> authors = db.getLocalAuthors(txn); + db.commitTransaction(txn); + return authors; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + identityLock.readLock().unlock(); + } + } + + public Collection<LocalGroup> getLocalGroups() throws DbException { + identityLock.readLock().lock(); + try { + T txn = db.startTransaction(); + try { + Collection<LocalGroup> groups = db.getLocalGroups(txn); + db.commitTransaction(txn); + return groups; + } catch(DbException e) { + db.abortTransaction(txn); + throw e; + } + } finally { + identityLock.readLock().unlock(); + } + } + public TransportProperties getLocalProperties(TransportId t) throws DbException { transportLock.readLock().lock(); diff --git a/briar-core/src/net/sf/briar/db/JdbcDatabase.java b/briar-core/src/net/sf/briar/db/JdbcDatabase.java index c9834381a5c653721f57e9990a06756f18e79324..d4dec6b438cbff6f21027803f91f2344ebaac5e3 100644 --- a/briar-core/src/net/sf/briar/db/JdbcDatabase.java +++ b/briar-core/src/net/sf/briar/db/JdbcDatabase.java @@ -41,6 +41,8 @@ import net.sf.briar.api.messaging.Author; import net.sf.briar.api.messaging.AuthorId; import net.sf.briar.api.messaging.Group; import net.sf.briar.api.messaging.GroupId; +import net.sf.briar.api.messaging.LocalAuthor; +import net.sf.briar.api.messaging.LocalGroup; import net.sf.briar.api.messaging.Message; import net.sf.briar.api.messaging.MessageId; import net.sf.briar.api.messaging.RetentionAck; @@ -60,10 +62,28 @@ import net.sf.briar.util.FileUtils; */ abstract class JdbcDatabase implements Database<Connection> { + // Locking: identity + private static final String CREATE_LOCAL_AUTHORS = + "CREATE TABLE localAuthors" + + " (authorId HASH NOT NULL," + + " name VARCHAR NOT NULL," + + " publicKey BINARY NOT NULL," + + " privateKey BINARY NOT NULL," + + " PRIMARY KEY (authorId))"; + + // Locking: identity + private static final String CREATE_LOCAL_GROUPS = + "CREATE TABLE localGroups" + + " (groupId HASH NOT NULL," + + " name VARCHAR NOT NULL," + + " publicKey BINARY NOT NULL," + + " privateKey BINARY NOT NULL," + + " PRIMARY KEY (groupId))"; + // Locking: contact // Dependents: message, retention, subscription, transport, window private static final String CREATE_CONTACTS = - "CREATE TABLE contacts " + "CREATE TABLE contacts" + " (contactId COUNTER," + " name VARCHAR NOT NULL," + " PRIMARY KEY (contactId))"; @@ -74,7 +94,7 @@ abstract class JdbcDatabase implements Database<Connection> { "CREATE TABLE groups" + " (groupId HASH NOT NULL," + " name VARCHAR NOT NULL," - + " key BINARY," // Null for unrestricted groups + + " publicKey BINARY," // Null for unrestricted groups + " PRIMARY KEY (groupId))"; // Locking: subscription @@ -95,7 +115,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " (contactId INT NOT NULL," + " groupId HASH NOT NULL," // Not a foreign key + " name VARCHAR NOT NULL," - + " key BINARY," // Null for unrestricted groups + + " publicKey BINARY," // Null for unrestricted groups + " PRIMARY KEY (contactId, groupId)," + " FOREIGN KEY (contactId)" + " REFERENCES contacts (contactId)" @@ -378,6 +398,8 @@ abstract class JdbcDatabase implements Database<Connection> { Statement s = null; try { s = txn.createStatement(); + s.executeUpdate(insertTypeNames(CREATE_LOCAL_AUTHORS)); + s.executeUpdate(insertTypeNames(CREATE_LOCAL_GROUPS)); s.executeUpdate(insertTypeNames(CREATE_CONTACTS)); s.executeUpdate(insertTypeNames(CREATE_GROUPS)); s.executeUpdate(insertTypeNames(CREATE_GROUP_VISIBILITIES)); @@ -672,6 +694,48 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public void addLocalAuthor(Connection txn, LocalAuthor a) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO localAuthors" + + " (authorId, name, publicKey, privateKey)" + + " VALUES (?, ?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, a.getId().getBytes()); + ps.setString(2, a.getName()); + ps.setBytes(3, a.getPublicKey()); + ps.setBytes(4, a.getPrivateKey()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + + public void addLocalGroup(Connection txn, LocalGroup g) + throws DbException { + PreparedStatement ps = null; + try { + String sql = "INSERT INTO localGroups" + + " (groupId, name, publicKey, privateKey)" + + " VALUES (?, ?, ?, ?)"; + ps = txn.prepareStatement(sql); + ps.setBytes(1, g.getId().getBytes()); + ps.setString(2, g.getName()); + ps.setBytes(3, g.getPublicKey()); + ps.setBytes(4, g.getPrivateKey()); + int affected = ps.executeUpdate(); + if(affected != 1) throw new DbStateException(); + ps.close(); + } catch(SQLException e) { + tryToClose(ps); + throw new DbException(e); + } + } + public void addMessageToAck(Connection txn, ContactId c, MessageId m) throws DbException { PreparedStatement ps = null; @@ -819,7 +883,8 @@ abstract class JdbcDatabase implements Database<Connection> { ps.close(); if(count > MAX_SUBSCRIPTIONS) throw new DbStateException(); if(count == MAX_SUBSCRIPTIONS) return false; - sql = "INSERT INTO groups (groupId, name, key) VALUES (?, ?, ?)"; + sql = "INSERT INTO groups (groupId, name, publicKey)" + + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getId().getBytes()); ps.setString(2, g.getName()); @@ -1154,7 +1219,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT name, key FROM groups WHERE groupId = ?"; + String sql = "SELECT name, publicKey FROM groups WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); rs = ps.executeQuery(); @@ -1222,6 +1287,56 @@ abstract class JdbcDatabase implements Database<Connection> { } } + public Collection<LocalAuthor> getLocalAuthors(Connection txn) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT authorId, name, publicKey, privateKey" + + " FROM localAuthors"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + List<LocalAuthor> authors = new ArrayList<LocalAuthor>(); + while(rs.next()) { + AuthorId id = new AuthorId(rs.getBytes(1)); + authors.add(new LocalAuthor(id, rs.getString(2), rs.getBytes(3), + rs.getBytes(4))); + } + rs.close(); + ps.close(); + return Collections.unmodifiableList(authors); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + + public Collection<LocalGroup> getLocalGroups(Connection txn) + throws DbException { + PreparedStatement ps = null; + ResultSet rs = null; + try { + String sql = "SELECT groupId, name, publicKey, privateKey" + + " FROM localGroups"; + ps = txn.prepareStatement(sql); + rs = ps.executeQuery(); + List<LocalGroup> groups = new ArrayList<LocalGroup>(); + while(rs.next()) { + GroupId id = new GroupId(rs.getBytes(1)); + groups.add(new LocalGroup(id, rs.getString(2), rs.getBytes(3), + rs.getBytes(4))); + } + rs.close(); + ps.close(); + return Collections.unmodifiableList(groups); + } catch(SQLException e) { + tryToClose(rs); + tryToClose(ps); + throw new DbException(e); + } + } + public TransportProperties getLocalProperties(Connection txn, TransportId t) throws DbException { PreparedStatement ps = null; @@ -1965,7 +2080,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT groupId, name, key FROM groups"; + String sql = "SELECT groupId, name, publicKey FROM groups"; ps = txn.prepareStatement(sql); rs = ps.executeQuery(); List<Group> subs = new ArrayList<Group>(); @@ -1990,7 +2105,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT groupId, name, key FROM contactGroups" + String sql = "SELECT groupId, name, publicKey FROM contactGroups" + " WHERE contactId = ?"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); @@ -2052,7 +2167,8 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT g.groupId, name, key, localVersion, txCount" + String sql = "SELECT g.groupId, name, publicKey," + + " localVersion, txCount" + " FROM groups AS g" + " JOIN groupVisibilities AS vis" + " ON g.groupId = vis.groupId" @@ -3023,7 +3139,8 @@ abstract class JdbcDatabase implements Database<Connection> { ps.executeUpdate(); // Store the new subscriptions, if any if(subs.isEmpty()) return; - sql = "INSERT INTO contactGroups (contactId, groupId, name, key)" + sql = "INSERT INTO contactGroups" + + " (contactId, groupId, name, publicKey)" + " VALUES (?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt());