From 85700dc985629f4afe9d7f995b8323bb45261e6a Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Sat, 23 Mar 2013 17:07:28 +0000
Subject: [PATCH] Store private keys for pseudonyms and restricted groups in
 the DB.

---
 .../groups/WriteGroupMessageActivity.java     |  15 +-
 .../sf/briar/api/db/DatabaseComponent.java    |  52 ++++---
 .../sf/briar/api/messaging/LocalAuthor.java   |  18 +++
 .../sf/briar/api/messaging/LocalGroup.java    |  18 +++
 briar-core/src/net/sf/briar/db/Database.java  |  32 +++++
 .../sf/briar/db/DatabaseComponentImpl.java    |  70 +++++++++
 .../src/net/sf/briar/db/JdbcDatabase.java     | 135 ++++++++++++++++--
 7 files changed, 310 insertions(+), 30 deletions(-)
 create mode 100644 briar-api/src/net/sf/briar/api/messaging/LocalAuthor.java
 create mode 100644 briar-api/src/net/sf/briar/api/messaging/LocalGroup.java

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 38b8ce59da..1668c92eda 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 add29d7abd..ba5f1848e9 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 0000000000..d60f44b455
--- /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 0000000000..27bdca0fad
--- /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 5ed55973f3..f3cd30c36c 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 f5184749ca..a4edf9750a 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 c9834381a5..d4dec6b438 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());
-- 
GitLab