diff --git a/api/net/sf/briar/api/protocol/MessageEncoder.java b/api/net/sf/briar/api/protocol/MessageEncoder.java index 7a2ff390d..c11c2bc14 100644 --- a/api/net/sf/briar/api/protocol/MessageEncoder.java +++ b/api/net/sf/briar/api/protocol/MessageEncoder.java @@ -6,10 +6,21 @@ import java.security.PrivateKey; public interface MessageEncoder { + /** Encodes an anonymous to an unrestricted group. */ Message encodeMessage(MessageId parent, Group group, byte[] body) - throws IOException; + throws IOException, GeneralSecurityException; + /** Encodes an anonymous message to a restricted group. */ + Message encodeMessage(MessageId parent, Group group, PrivateKey groupKey, + byte[] body) throws IOException, GeneralSecurityException; + + /** Encodes a pseudonymous message to an unrestricted group. */ Message encodeMessage(MessageId parent, Group group, Author author, - PrivateKey privateKey, byte[] body) throws IOException, + PrivateKey authorKey, byte[] body) throws IOException, GeneralSecurityException; + + /** Encode a pseudonymous message to a restricted group. */ + Message encodeMessage(MessageId parent, Group group, PrivateKey groupKey, + Author author, PrivateKey authorKey, byte[] body) + throws IOException, GeneralSecurityException; } diff --git a/components/net/sf/briar/protocol/MessageEncoderImpl.java b/components/net/sf/briar/protocol/MessageEncoderImpl.java index 458cf2a63..5c2149ad5 100644 --- a/components/net/sf/briar/protocol/MessageEncoderImpl.java +++ b/components/net/sf/briar/protocol/MessageEncoderImpl.java @@ -34,30 +34,31 @@ class MessageEncoderImpl implements MessageEncoder { } public Message encodeMessage(MessageId parent, Group group, byte[] body) - throws IOException { - long timestamp = System.currentTimeMillis(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Writer w = writerFactory.createWriter(out); - // Write the message - w.writeUserDefinedTag(Tags.MESSAGE); - parent.writeTo(w); - group.writeTo(w); - w.writeNull(); // No author - w.writeInt64(timestamp); - w.writeBytes(body); - w.writeNull(); // No author's signature - byte[] raw = out.toByteArray(); - // The message ID is the hash of the entire message - messageDigest.reset(); - messageDigest.update(raw); - MessageId id = new MessageId(messageDigest.digest()); - return new MessageImpl(id, parent, group.getId(), AuthorId.NONE, - timestamp, raw); + throws IOException, GeneralSecurityException { + return encodeMessage(parent, group, null, null, null, body); + } + + public Message encodeMessage(MessageId parent, Group group, + PrivateKey groupKey, byte[] body) throws IOException, + GeneralSecurityException { + return encodeMessage(parent, group, groupKey, null, null, body); } public Message encodeMessage(MessageId parent, Group group, Author author, - PrivateKey privateKey, byte[] body) throws IOException, + PrivateKey authorKey, byte[] body) throws IOException, GeneralSecurityException { + return encodeMessage(parent, group, null, author, authorKey, body); + } + + public Message encodeMessage(MessageId parent, Group group, + PrivateKey groupKey, Author author, PrivateKey authorKey, + byte[] body) throws IOException, GeneralSecurityException { + + if((author == null) != (authorKey == null)) + throw new IllegalArgumentException(); + if((group.getPublicKey() == null) != (groupKey == null)) + throw new IllegalArgumentException(); + long timestamp = System.currentTimeMillis(); ByteArrayOutputStream out = new ByteArrayOutputStream(); Writer w = writerFactory.createWriter(out); @@ -65,29 +66,32 @@ class MessageEncoderImpl implements MessageEncoder { w.writeUserDefinedTag(Tags.MESSAGE); parent.writeTo(w); group.writeTo(w); - author.writeTo(w); + if(author == null) w.writeNull(); + else author.writeTo(w); w.writeInt64(timestamp); w.writeBytes(body); - // Sign the message - byte[] signable = out.toByteArray(); - signature.initSign(privateKey); - signature.update(signable); - byte[] sig = signature.sign(); - signable = null; - // Write the signature - w.writeBytes(sig); + // Sign the message with the author's private key, if there is one + if(authorKey == null) { + w.writeNull(); + } else { + signature.initSign(authorKey); + signature.update(out.toByteArray()); + w.writeBytes(signature.sign()); + } + // Sign the message with the group's private key, if there is one + if(groupKey == null) { + w.writeNull(); + } else { + signature.initSign(groupKey); + signature.update(out.toByteArray()); + w.writeBytes(signature.sign()); + } + // Hash the message, including the signatures, to get the message ID byte[] raw = out.toByteArray(); - // The message ID is the hash of the entire message messageDigest.reset(); messageDigest.update(raw); MessageId id = new MessageId(messageDigest.digest()); - // The author ID is the hash of the author object - out.reset(); - w = writerFactory.createWriter(out); - author.writeTo(w); - messageDigest.reset(); - messageDigest.update(out.toByteArray()); - AuthorId authorId = new AuthorId(messageDigest.digest()); + AuthorId authorId = author == null ? AuthorId.NONE : author.getId(); return new MessageImpl(id, parent, group.getId(), authorId, timestamp, raw); } diff --git a/components/net/sf/briar/protocol/MessageReader.java b/components/net/sf/briar/protocol/MessageReader.java index 8e3125533..d80a6dfa5 100644 --- a/components/net/sf/briar/protocol/MessageReader.java +++ b/components/net/sf/briar/protocol/MessageReader.java @@ -5,7 +5,6 @@ import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.PublicKey; import java.security.Signature; -import java.security.SignatureException; import net.sf.briar.api.crypto.CryptoComponent; import net.sf.briar.api.crypto.KeyParser; @@ -64,12 +63,18 @@ class MessageReader implements ObjectReader { if(timestamp < 0L) throw new FormatException(); // Skip the message body r.readBytes(); - // Record the length of the signed data - int messageLength = (int) counting.getCount(); + // Record the length of the data covered by the author's signature + int signedByAuthor = (int) counting.getCount(); // Read the author's signature, if there is one byte[] authorSig = null; if(author == null) r.readNull(); else authorSig = r.readBytes(); + // Record the length of the data covered by the group's signature + int signedByGroup = (int) counting.getCount(); + // Read the group's signature, if there is one + byte[] groupSig = null; + if(group.getPublicKey() == null) r.readNull(); + else groupSig = r.readBytes(); // That's all, folks r.removeConsumer(counting); r.removeConsumer(copying); @@ -77,16 +82,26 @@ class MessageReader implements ObjectReader { // Verify the author's signature, if there is one if(author != null) { try { - PublicKey publicKey = - keyParser.parsePublicKey(author.getPublicKey()); - signature.initVerify(publicKey); - signature.update(raw, 0, messageLength); - if(!signature.verify(authorSig)) throw new SignatureException(); + PublicKey k = keyParser.parsePublicKey(author.getPublicKey()); + signature.initVerify(k); + signature.update(raw, 0, signedByAuthor); + if(!signature.verify(authorSig)) throw new FormatException(); } catch(GeneralSecurityException e) { throw new FormatException(); } } - // Hash the message, including the signature, to get the message ID + // Verify the group's signature, if there is one + if(group.getPublicKey() != null) { + try { + PublicKey k = keyParser.parsePublicKey(group.getPublicKey()); + signature.initVerify(k); + signature.update(raw, 0, signedByGroup); + if(!signature.verify(groupSig)) throw new FormatException(); + } catch(GeneralSecurityException e) { + throw new FormatException(); + } + } + // Hash the message, including the signatures, to get the message ID messageDigest.reset(); messageDigest.update(raw); MessageId id = new MessageId(messageDigest.digest()); diff --git a/test/net/sf/briar/protocol/FileReadWriteTest.java b/test/net/sf/briar/protocol/FileReadWriteTest.java index 92cdf83f1..4fe36afc2 100644 --- a/test/net/sf/briar/protocol/FileReadWriteTest.java +++ b/test/net/sf/briar/protocol/FileReadWriteTest.java @@ -4,7 +4,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.security.KeyPair; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -50,8 +52,6 @@ public class FileReadWriteTest extends TestCase { private final File file = new File(testDir, "foo"); private final BatchId ack = new BatchId(TestUtils.getRandomId()); - private final String authorName = "Foo Bar"; - private final String messageBody = "This is the message body! Wooooooo!"; private final long start = System.currentTimeMillis(); private final ReaderFactory readerFactory; @@ -59,8 +59,10 @@ public class FileReadWriteTest extends TestCase { private final PacketWriterFactory packetWriterFactory; private final CryptoComponent crypto; private final Author author; - private final Group group; - private final Message message; + private final Group group, group1; + private final Message message, message1, message2, message3; + private final String authorName = "Alice"; + private final String messageBody = "Hello world"; public FileReadWriteTest() throws Exception { super(); @@ -73,18 +75,28 @@ public class FileReadWriteTest extends TestCase { crypto = i.getInstance(CryptoComponent.class); assertEquals(crypto.getMessageDigest().getDigestLength(), UniqueId.LENGTH); - // Create a group + // Create two groups: one restricted, one unrestricted GroupFactory groupFactory = i.getInstance(GroupFactory.class); - group = groupFactory.createGroup("Group name", null); + group = groupFactory.createGroup("Unrestricted group", null); + KeyPair groupKeyPair = crypto.generateKeyPair(); + group1 = groupFactory.createGroup("Restricted group", + groupKeyPair.getPublic().getEncoded()); // Create an author AuthorFactory authorFactory = i.getInstance(AuthorFactory.class); - KeyPair keyPair = crypto.generateKeyPair(); + KeyPair authorKeyPair = crypto.generateKeyPair(); author = authorFactory.createAuthor(authorName, - keyPair.getPublic().getEncoded()); - // Create and encode a test message, signed by the author + authorKeyPair.getPublic().getEncoded()); + // Create two messages to each group: one anonymous, one pseudonymous MessageEncoder messageEncoder = i.getInstance(MessageEncoder.class); - message = messageEncoder.encodeMessage(MessageId.NONE, group, author, - keyPair.getPrivate(), messageBody.getBytes("UTF-8")); + message = messageEncoder.encodeMessage(MessageId.NONE, group, + messageBody.getBytes("UTF-8")); + message1 = messageEncoder.encodeMessage(MessageId.NONE, group1, + groupKeyPair.getPrivate(), messageBody.getBytes("UTF-8")); + message2 = messageEncoder.encodeMessage(MessageId.NONE, group, author, + authorKeyPair.getPrivate(), messageBody.getBytes("UTF-8")); + message3 = messageEncoder.encodeMessage(MessageId.NONE, group1, + groupKeyPair.getPrivate(), author, authorKeyPair.getPrivate(), + messageBody.getBytes("UTF-8")); } @Before @@ -102,11 +114,17 @@ public class FileReadWriteTest extends TestCase { BatchWriter b = packetWriterFactory.createBatchWriter(out); assertTrue(b.writeMessage(message.getBytes())); + assertTrue(b.writeMessage(message1.getBytes())); + assertTrue(b.writeMessage(message2.getBytes())); + assertTrue(b.writeMessage(message3.getBytes())); b.finish(); SubscriptionWriter s = packetWriterFactory.createSubscriptionWriter(out); - s.writeSubscriptions(Collections.singleton(group)); + Collection subs = new ArrayList(); + subs.add(group); + subs.add(group1); + s.writeSubscriptions(subs); TransportWriter t = packetWriterFactory.createTransportWriter(out); t.writeTransports(Collections.singletonMap("foo", "bar")); @@ -151,22 +169,23 @@ public class FileReadWriteTest extends TestCase { // Read the batch assertTrue(reader.hasUserDefined(Tags.BATCH)); Batch b = reader.readUserDefined(Tags.BATCH, Batch.class); - Iterator i = b.getMessages().iterator(); - assertTrue(i.hasNext()); - Message m = i.next(); - assertEquals(message.getId(), m.getId()); - assertEquals(message.getParent(), m.getParent()); - assertEquals(message.getGroup(), m.getGroup()); - assertEquals(message.getAuthor(), m.getAuthor()); - assertEquals(message.getTimestamp(), m.getTimestamp()); - assertTrue(Arrays.equals(message.getBytes(), m.getBytes())); - assertFalse(i.hasNext()); - + Collection messages = b.getMessages(); + assertEquals(4, messages.size()); + Iterator i = messages.iterator(); + checkMessageEquality(message, i.next()); + checkMessageEquality(message1, i.next()); + checkMessageEquality(message2, i.next()); + checkMessageEquality(message3, i.next()); + // Read the subscriptions update assertTrue(reader.hasUserDefined(Tags.SUBSCRIPTIONS)); Subscriptions s = reader.readUserDefined(Tags.SUBSCRIPTIONS, Subscriptions.class); - assertEquals(Collections.singletonList(group), s.getSubscriptions()); + Collection subs = s.getSubscriptions(); + assertEquals(2, subs.size()); + Iterator i1 = subs.iterator(); + checkGroupEquality(group, i1.next()); + checkGroupEquality(group1, i1.next()); assertTrue(s.getTimestamp() > start); assertTrue(s.getTimestamp() <= System.currentTimeMillis()); @@ -185,4 +204,22 @@ public class FileReadWriteTest extends TestCase { public void tearDown() { TestUtils.deleteTestDirectory(testDir); } + + private void checkMessageEquality(Message m1, Message m2) { + assertEquals(m1.getId(), m2.getId()); + assertEquals(m1.getParent(), m2.getParent()); + assertEquals(m1.getGroup(), m2.getGroup()); + assertEquals(m1.getAuthor(), m2.getAuthor()); + assertEquals(m1.getTimestamp(), m2.getTimestamp()); + assertTrue(Arrays.equals(m1.getBytes(), m2.getBytes())); + } + + private void checkGroupEquality(Group g1, Group g2) { + assertEquals(g1.getId(), g2.getId()); + assertEquals(g1.getName(), g2.getName()); + byte[] k1 = g1.getPublicKey(); + byte[] k2 = g2.getPublicKey(); + if(k1 == null) assertNull(k2); + else assertTrue(Arrays.equals(k1, k2)); + } }