diff --git a/api/net/sf/briar/api/crypto/KeyParser.java b/api/net/sf/briar/api/crypto/KeyParser.java new file mode 100644 index 0000000000000000000000000000000000000000..2363dee5d88846a26cf8b685f85a3afeae94e9c1 --- /dev/null +++ b/api/net/sf/briar/api/crypto/KeyParser.java @@ -0,0 +1,9 @@ +package net.sf.briar.api.crypto; + +import java.security.GeneralSecurityException; +import java.security.PublicKey; + +public interface KeyParser { + + PublicKey parsePublicKey(byte[] encodedKey) throws GeneralSecurityException; +} diff --git a/api/net/sf/briar/api/protocol/Message.java b/api/net/sf/briar/api/protocol/Message.java index 715f1115aed2177ba5fe48b4189c688af13ab80b..3b1739fdfdef30f3169bb52711962c97a9d6ae6e 100644 --- a/api/net/sf/briar/api/protocol/Message.java +++ b/api/net/sf/briar/api/protocol/Message.java @@ -4,6 +4,8 @@ import net.sf.briar.api.serial.Raw; public interface Message extends Raw { + static final int MAX_SIZE = 1024 * 1023; // Not a typo + /** Returns the message's unique identifier. */ MessageId getId(); diff --git a/api/net/sf/briar/api/protocol/MessageEncoder.java b/api/net/sf/briar/api/protocol/MessageEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..95b26e1bf9f828d4ce3cc532ddebdb2f16c8433e --- /dev/null +++ b/api/net/sf/briar/api/protocol/MessageEncoder.java @@ -0,0 +1,12 @@ +package net.sf.briar.api.protocol; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; + +public interface MessageEncoder { + + Message encodeMessage(MessageId parent, GroupId group, String nick, + KeyPair keyPair, byte[] body) throws IOException, + GeneralSecurityException; +} diff --git a/api/net/sf/briar/api/protocol/MessageParser.java b/api/net/sf/briar/api/protocol/MessageParser.java index 95a44e4cdd3528501098983de80ef60e31747a21..756ed84d3c7cadab27ff3e041c3568e936417b23 100644 --- a/api/net/sf/briar/api/protocol/MessageParser.java +++ b/api/net/sf/briar/api/protocol/MessageParser.java @@ -1,10 +1,9 @@ package net.sf.briar.api.protocol; -import java.security.SignatureException; - -import net.sf.briar.api.serial.FormatException; +import java.io.IOException; +import java.security.GeneralSecurityException; public interface MessageParser { - Message parseMessage(byte[] body) throws FormatException, SignatureException; + Message parseMessage(byte[] raw) throws IOException, GeneralSecurityException; } diff --git a/components/net/sf/briar/protocol/MessageEncoderImpl.java b/components/net/sf/briar/protocol/MessageEncoderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..fac373dd1c4b409cdbdfa0cc2dfa54ea21f139b9 --- /dev/null +++ b/components/net/sf/briar/protocol/MessageEncoderImpl.java @@ -0,0 +1,63 @@ +package net.sf.briar.protocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.Signature; + +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageEncoder; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; + +class MessageEncoderImpl implements MessageEncoder { + + private final Signature signature; + private final MessageDigest messageDigest; + private final WriterFactory writerFactory; + + MessageEncoderImpl(Signature signature, MessageDigest messageDigest, + WriterFactory writerFactory) { + this.signature = signature; + this.messageDigest = messageDigest; + this.writerFactory = writerFactory; + } + + public Message encodeMessage(MessageId parent, GroupId group, String nick, + KeyPair keyPair, byte[] body) throws IOException, + GeneralSecurityException { + long timestamp = System.currentTimeMillis(); + byte[] encodedKey = keyPair.getPublic().getEncoded(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeRaw(parent); + w.writeRaw(group); + w.writeInt64(timestamp); + w.writeUtf8(nick); + w.writeRaw(encodedKey); + w.writeRaw(body); + byte[] signable = out.toByteArray(); + signature.initSign(keyPair.getPrivate()); + signature.update(signable); + byte[] sig = signature.sign(); + w.writeRaw(sig); + byte[] raw = out.toByteArray(); + w.close(); + // 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's nick and public key + messageDigest.reset(); + messageDigest.update(nick.getBytes("UTF-8")); + messageDigest.update((byte) 0); // Null separator + messageDigest.update(encodedKey); + AuthorId author = new AuthorId(messageDigest.digest()); + return new MessageImpl(id, parent, group, author, timestamp, raw); + } +} diff --git a/components/net/sf/briar/protocol/MessageImpl.java b/components/net/sf/briar/protocol/MessageImpl.java index 3fcbc273b96b3264944f65d7168af945c78965b4..653062c4181b231d07ac330583325c0f65fd5a49 100644 --- a/components/net/sf/briar/protocol/MessageImpl.java +++ b/components/net/sf/briar/protocol/MessageImpl.java @@ -12,16 +12,16 @@ public class MessageImpl implements Message { private final GroupId group; private final AuthorId author; private final long timestamp; - private final byte[] body; + private final byte[] raw; public MessageImpl(MessageId id, MessageId parent, GroupId group, - AuthorId author, long timestamp, byte[] body) { + AuthorId author, long timestamp, byte[] raw) { this.id = id; this.parent = parent; this.group = group; this.author = author; this.timestamp = timestamp; - this.body = body; + this.raw = raw; } public MessageId getId() { @@ -45,11 +45,11 @@ public class MessageImpl implements Message { } public int getSize() { - return body.length; + return raw.length; } public byte[] getBytes() { - return body; + return raw; } @Override diff --git a/components/net/sf/briar/protocol/MessageParserImpl.java b/components/net/sf/briar/protocol/MessageParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..f9b138a621da05bce164d88c6a96d15754e20935 --- /dev/null +++ b/components/net/sf/briar/protocol/MessageParserImpl.java @@ -0,0 +1,84 @@ +package net.sf.briar.protocol; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +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.KeyParser; +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.protocol.MessageParser; +import net.sf.briar.api.protocol.UniqueId; +import net.sf.briar.api.serial.FormatException; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; + +class MessageParserImpl implements MessageParser { + + private final KeyParser keyParser; + private final Signature signature; + private final MessageDigest messageDigest; + private final ReaderFactory readerFactory; + + MessageParserImpl(KeyParser keyParser, Signature signature, + MessageDigest messageDigest, ReaderFactory readerFactory) { + this.keyParser = keyParser; + this.signature = signature; + this.messageDigest = messageDigest; + this.readerFactory = readerFactory; + } + + public Message parseMessage(byte[] raw) throws IOException, + GeneralSecurityException { + if(raw.length > Message.MAX_SIZE) throw new FormatException(); + ByteArrayInputStream in = new ByteArrayInputStream(raw); + Reader r = readerFactory.createReader(in); + // Read the parent message ID + byte[] idBytes = r.readRaw(); + if(idBytes.length != UniqueId.LENGTH) throw new FormatException(); + MessageId parent = new MessageId(idBytes); + // Read the group ID + idBytes = r.readRaw(); + if(idBytes.length != UniqueId.LENGTH) throw new FormatException(); + GroupId group = new GroupId(idBytes); + // Read the timestamp + long timestamp = r.readInt64(); + // Hash the author's nick and public key to get the author ID + String nick = r.readUtf8(); + byte[] encodedKey = r.readRaw(); + PublicKey publicKey = keyParser.parsePublicKey(encodedKey); + messageDigest.reset(); + messageDigest.update(nick.getBytes("UTF-8")); + messageDigest.update((byte) 0); // Null separator + messageDigest.update(encodedKey); + AuthorId author = new AuthorId(messageDigest.digest()); + // Skip the message body + r.readRaw(); + // Read the signature and work out how long the signed message is + byte[] sig = r.readRaw(); + int length = raw.length - sig.length - bytesToEncode(sig.length); + // Verify the signature + signature.initVerify(publicKey); + signature.update(raw, 0, length); + if(!signature.verify(sig)) throw new SignatureException(); + // Hash the message, including the signature, to get the message ID + messageDigest.reset(); + messageDigest.update(raw); + MessageId id = new MessageId(messageDigest.digest()); + return new MessageImpl(id, parent, group, author, timestamp, raw); + } + + // FIXME: Work out a better way of doing this + private int bytesToEncode(int i) { + if(i >= 0 && i <= Byte.MAX_VALUE) return 2; + if(i >= Byte.MIN_VALUE && i <= Byte.MAX_VALUE) return 3; + if(i >= Short.MIN_VALUE && i <= Short.MAX_VALUE) return 4; + return 6; + } +} diff --git a/test/net/sf/briar/protocol/BundleReadWriteTest.java b/test/net/sf/briar/protocol/BundleReadWriteTest.java index 69ddd1241579ee7631242eba5543977c2ff43609..fcb26721e2d14064eff49a8cacad20c626bd70c0 100644 --- a/test/net/sf/briar/protocol/BundleReadWriteTest.java +++ b/test/net/sf/briar/protocol/BundleReadWriteTest.java @@ -5,19 +5,23 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.RandomAccessFile; import java.security.GeneralSecurityException; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; import java.security.Signature; -import java.security.SignatureException; +import java.security.spec.EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.Map; import java.util.Set; import junit.framework.TestCase; import net.sf.briar.TestUtils; -import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.crypto.KeyParser; import net.sf.briar.api.protocol.Batch; import net.sf.briar.api.protocol.BatchBuilder; import net.sf.briar.api.protocol.BatchId; @@ -27,11 +31,12 @@ import net.sf.briar.api.protocol.GroupId; import net.sf.briar.api.protocol.Header; import net.sf.briar.api.protocol.HeaderBuilder; import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageEncoder; import net.sf.briar.api.protocol.MessageId; import net.sf.briar.api.protocol.MessageParser; import net.sf.briar.api.protocol.UniqueId; -import net.sf.briar.api.serial.FormatException; import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; import net.sf.briar.api.serial.Writer; import net.sf.briar.api.serial.WriterFactory; import net.sf.briar.serial.ReaderFactoryImpl; @@ -59,27 +64,34 @@ public class BundleReadWriteTest extends TestCase { private final Set<GroupId> subs = Collections.singleton(sub); private final Map<String, String> transports = Collections.singletonMap("foo", "bar"); + private final String nick = "Foo Bar"; + private final String messageBody = "This is the message body! Wooooooo!"; - private final MessageId messageId = new MessageId(TestUtils.getRandomId()); - private final AuthorId authorId = new AuthorId(TestUtils.getRandomId()); - private final long timestamp = System.currentTimeMillis(); - private final byte[] messageBody = new byte[123]; - private final Message message = new MessageImpl(messageId, MessageId.NONE, - sub, authorId, timestamp, messageBody); - - // FIXME: This test should not depend on an impl in another component + // FIXME: This test should not depend on impls in another component + private final ReaderFactory rf = new ReaderFactoryImpl(); private final WriterFactory wf = new WriterFactoryImpl(); private final KeyPair keyPair; private final Signature sig; private final MessageDigest digest; + private final KeyParser keyParser; + private final Message message; - public BundleReadWriteTest() throws NoSuchAlgorithmException { + public BundleReadWriteTest() throws Exception { super(); keyPair = KeyPairGenerator.getInstance(KEY_PAIR_ALGO).generateKeyPair(); sig = Signature.getInstance(SIGNATURE_ALGO); digest = MessageDigest.getInstance(DIGEST_ALGO); + keyParser = new KeyParser() { + public PublicKey parsePublicKey(byte[] encodedKey) throws GeneralSecurityException { + EncodedKeySpec e = new X509EncodedKeySpec(encodedKey); + return KeyFactory.getInstance(KEY_PAIR_ALGO).generatePublic(e); + } + }; assertEquals(digest.getDigestLength(), UniqueId.LENGTH); + MessageEncoder messageEncoder = new MessageEncoderImpl(sig, digest, wf); + message = messageEncoder.encodeMessage(MessageId.NONE, sub, nick, + keyPair, messageBody.getBytes("UTF-8")); } @Before @@ -108,7 +120,7 @@ public class BundleReadWriteTest extends TestCase { w.close(); assertTrue(bundle.exists()); - assertTrue(bundle.length() > messageBody.length); + assertTrue(bundle.length() > message.getSize()); } @Test @@ -116,13 +128,8 @@ public class BundleReadWriteTest extends TestCase { testWriteBundle(); - MessageParser messageParser = new MessageParser() { - public Message parseMessage(byte[] body) throws FormatException, - SignatureException { - // FIXME: Really parse the message - return message; - } - }; + MessageParser messageParser = + new MessageParserImpl(keyParser, sig, digest, rf); Provider<HeaderBuilder> headerBuilderProvider = new Provider<HeaderBuilder>() { public HeaderBuilder get() { @@ -146,7 +153,16 @@ public class BundleReadWriteTest extends TestCase { assertEquals(subs, h.getSubscriptions()); assertEquals(transports, h.getTransports()); Batch b = r.getNextBatch(); - assertEquals(Collections.singletonList(message), b.getMessages()); + Iterator<Message> 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()); assertNull(r.getNextBatch()); r.close(); } @@ -158,19 +174,14 @@ public class BundleReadWriteTest extends TestCase { testWriteBundle(); RandomAccessFile f = new RandomAccessFile(bundle, "rw"); - f.seek(bundle.length() - 50); + f.seek(bundle.length() - 150); byte b = f.readByte(); - f.seek(bundle.length() - 50); + f.seek(bundle.length() - 150); f.writeByte(b + 1); f.close(); - MessageParser messageParser = new MessageParser() { - public Message parseMessage(byte[] body) throws FormatException, - SignatureException { - // FIXME: Really parse the message - return message; - } - }; + MessageParser messageParser = + new MessageParserImpl(keyParser, sig, digest, rf); Provider<HeaderBuilder> headerBuilderProvider = new Provider<HeaderBuilder>() { public HeaderBuilder get() {