mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-15 12:19:54 +01:00
Separate the sync layer from its clients. #112
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -33,13 +33,13 @@ 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 {
|
||||
private Object parseValue(byte[] b) throws FormatException {
|
||||
if (b == REMOVE) return null;
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(b);
|
||||
Object o = parseObject(in);
|
||||
|
||||
@@ -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 addGroup(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.
|
||||
@@ -265,29 +274,12 @@ interface Database<T> {
|
||||
Group getGroup(T txn, GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all groups to which the user subscribes, excluding inboxes.
|
||||
* Returns all groups to which the user subscribes.
|
||||
* <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.
|
||||
* <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;
|
||||
|
||||
/**
|
||||
* Returns the local pseudonym with the given ID.
|
||||
* <p>
|
||||
@@ -319,19 +311,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 +380,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 +478,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 +521,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 +629,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 +643,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.
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.Metadata;
|
||||
import org.briarproject.api.db.NoSuchContactException;
|
||||
import org.briarproject.api.db.NoSuchLocalAuthorException;
|
||||
import org.briarproject.api.db.NoSuchMessageException;
|
||||
@@ -25,6 +26,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 +41,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 +168,22 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
return c;
|
||||
}
|
||||
|
||||
public void addGroup(ContactId c, Group g) throws DbException {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
T txn = db.startTransaction();
|
||||
try {
|
||||
db.addGroup(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 +223,19 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
eventBus.broadcast(new LocalAuthorAddedEvent(a.getId()));
|
||||
}
|
||||
|
||||
public void addLocalMessage(Message m) throws DbException {
|
||||
public void addLocalMessage(Message m, ClientId c, Metadata meta)
|
||||
throws DbException {
|
||||
boolean duplicate, subscribed;
|
||||
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);
|
||||
subscribed = db.containsGroup(txn, m.getGroupId());
|
||||
if (!duplicate && subscribed) {
|
||||
addMessage(txn, m, null);
|
||||
db.mergeMessageMetadata(txn, m.getId(), meta);
|
||||
}
|
||||
db.commitTransaction(txn);
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
@@ -223,26 +246,21 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
if (!duplicate && subscribed) {
|
||||
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)) {
|
||||
@@ -595,46 +613,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public GroupId getInboxGroupId(ContactId c) 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);
|
||||
db.commitTransaction(txn);
|
||||
return inbox;
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<MessageHeader> getInboxMessageHeaders(ContactId c)
|
||||
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);
|
||||
db.commitTransaction(txn);
|
||||
return headers;
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public LocalAuthor getLocalAuthor(AuthorId a) throws DbException {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
@@ -710,16 +688,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getMessageBody(MessageId m) throws DbException {
|
||||
public Collection<MessageId> getMessagesToValidate(ClientId c)
|
||||
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);
|
||||
Collection<MessageId> ids = db.getMessagesToValidate(txn, c);
|
||||
db.commitTransaction(txn);
|
||||
return body;
|
||||
return ids;
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
throw e;
|
||||
@@ -729,7 +706,26 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<MessageHeader> getMessageHeaders(GroupId g)
|
||||
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 {
|
||||
@@ -737,10 +733,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
try {
|
||||
if (!db.containsGroup(txn, g))
|
||||
throw new NoSuchSubscriptionException();
|
||||
Collection<MessageHeader> headers =
|
||||
db.getMessageHeaders(txn, g);
|
||||
Map<MessageId, Metadata> metadata =
|
||||
db.getMessageMetadata(txn, g);
|
||||
db.commitTransaction(txn);
|
||||
return headers;
|
||||
return metadata;
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
throw e;
|
||||
@@ -750,16 +746,61 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getReadFlag(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();
|
||||
boolean read = db.getReadFlag(txn, m);
|
||||
Metadata metadata = db.getMessageMetadata(txn, m);
|
||||
db.commitTransaction(txn);
|
||||
return read;
|
||||
return metadata;
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
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<MessageStatus> statuses =
|
||||
db.getMessageStatus(txn, c, g);
|
||||
db.commitTransaction(txn);
|
||||
return statuses;
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
MessageStatus status = db.getMessageStatus(txn, c, m);
|
||||
db.commitTransaction(txn);
|
||||
return status;
|
||||
} catch (DbException e) {
|
||||
db.abortTransaction(txn);
|
||||
throw e;
|
||||
@@ -862,23 +903,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 +967,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 +1041,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 +1055,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 +1212,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 +1256,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 +1289,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
eventBus.broadcast(new TransportRemovedEvent(t));
|
||||
}
|
||||
|
||||
public void setInboxGroup(ContactId c, Group g) throws DbException {
|
||||
public void setMessageValidity(Message m, ClientId c, boolean valid)
|
||||
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 {
|
||||
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 +1306,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
eventBus.broadcast(new MessageValidatedEvent(m, c, false, valid));
|
||||
}
|
||||
|
||||
public void setRemoteProperties(ContactId c,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,"
|
||||
+ " length INT NOT NULL,"
|
||||
+ " bodyStart INT NOT NULL,"
|
||||
+ " bodyLength INT NOT NULL,"
|
||||
+ " raw BLOB NOT NULL,"
|
||||
+ " local BOOLEAN NOT NULL,"
|
||||
+ " read BOOLEAN NOT NULL,"
|
||||
+ " valid INT NOT NULL,"
|
||||
+ " length INT NOT NULL,"
|
||||
+ " raw BLOB 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 addGroup(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());
|
||||
@@ -1152,7 +1160,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT DISTINCT cg.groupId, cg.name, cg.salt"
|
||||
String sql = "SELECT DISTINCT"
|
||||
+ " cg.groupId, cg.clientId, cg.descriptor"
|
||||
+ " FROM contactGroups AS cg"
|
||||
+ " LEFT OUTER JOIN groups AS g"
|
||||
+ " ON cg.groupId = g.groupId"
|
||||
@@ -1165,9 +1174,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));
|
||||
}
|
||||
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);
|
||||
@@ -1302,20 +1312,15 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
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, clientId, descriptor FROM groups";
|
||||
ps = txn.prepareStatement(sql);
|
||||
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));
|
||||
ClientId clientId = new ClientId(rs.getBytes(2));
|
||||
byte[] descriptor = rs.getBytes(3);
|
||||
groups.add(new Group(id, clientId, descriptor));
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
@@ -1327,103 +1332,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 +1446,35 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getMessageBody(Connection txn, MessageId m)
|
||||
throws DbException {
|
||||
public Map<MessageId, Metadata> getMessageMetadata(Connection txn,
|
||||
GroupId g) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
String sql = "SELECT bodyStart, bodyLength, raw FROM messages"
|
||||
+ " WHERE messageId = ?";
|
||||
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, m.getBytes());
|
||||
ps.setBytes(1, g.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();
|
||||
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 body;
|
||||
return Collections.unmodifiableMap(all);
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs);
|
||||
tryToClose(ps);
|
||||
@@ -1564,60 +1482,81 @@ 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 Metadata getMessageMetadata(Connection txn, MessageId m)
|
||||
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 key, value"
|
||||
+ " FROM messageMetadata"
|
||||
+ " WHERE messageId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setBytes(1, m.getBytes());
|
||||
rs = ps.executeQuery();
|
||||
Metadata metadata = new Metadata();
|
||||
while (rs.next()) metadata.put(rs.getString(1), rs.getBytes(2));
|
||||
rs.close();
|
||||
ps.close();
|
||||
return metadata;
|
||||
} catch (SQLException e) {
|
||||
tryToClose(rs);
|
||||
tryToClose(ps);
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<MessageStatus> getMessageStatus(Connection txn,
|
||||
ContactId c, GroupId g) throws DbException {
|
||||
PreparedStatement ps = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
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 +1604,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 +1666,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 +1693,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 +1722,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 +1739,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 +1791,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 +1912,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 +1933,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 +2153,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 +2313,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 +2408,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 +2685,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 +2786,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 +2811,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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,79 +2,280 @@ 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 {
|
||||
// TODO: Get groups by client ID
|
||||
Collection<Group> groups = db.getAvailableGroups();
|
||||
List<Forum> forums = new ArrayList<Forum>(groups.size());
|
||||
for (Group g : groups) forums.add(new ForumImpl(g));
|
||||
for (Group g : groups) {
|
||||
if (g.getClientId().equals(CLIENT_ID)) {
|
||||
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 {
|
||||
// TODO: Get groups by client ID
|
||||
Collection<Group> groups = db.getGroups();
|
||||
List<Forum> forums = new ArrayList<Forum>(groups.size());
|
||||
for (Group g : groups) forums.add(new ForumImpl(g));
|
||||
for (Group g : groups) {
|
||||
if (g.getClientId().equals(CLIENT_ID)) {
|
||||
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 +290,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
184
briar-core/src/org/briarproject/forum/ForumPostValidator.java
Normal file
184
briar-core/src/org/briarproject/forum/ForumPostValidator.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -2,88 +2,206 @@ 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 addLocalMessage(Message m) throws DbException {
|
||||
db.addLocalMessage(m);
|
||||
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.addGroup(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 PrivateConversation getConversation(GroupId g) throws DbException {
|
||||
return new PrivateConversationImpl(db.getGroup(g));
|
||||
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 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);
|
||||
// TODO: Make this more efficient
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
MessageFactoryImpl(CryptoComponent crypto) {
|
||||
this.crypto = crypto;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
162
briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
Normal file
162
briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user