diff --git a/PROTOCOL.md b/PROTOCOL.md index b7f9dc4..e4d8de7 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -37,7 +37,7 @@ the AES key is wrapped with the recipient's public RSA key. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / / - \ Header (variable length) \ + \ Header (76 bytes) \ / / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / / @@ -52,19 +52,17 @@ the AES key is wrapped with the recipient's public RSA key. ### Header -Every message starts with one 32 bit word indicating the message +Every message starts with one 76 byte header indicating the message version, type and ID, followed by the length of the message. The header is in network byte order, i.e. big endian. 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Ver | Type | Hop Limit | Hop Count | + | Version | Type | Hop Limit | Hop Count | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Time | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | Origin Address | | | @@ -73,15 +71,15 @@ header is in network byte order, i.e. big endian. | Target Address | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sequence Number | Metric | Reserved | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Body Length | + | Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -Ver specifies the protocol version number. This is currently 0. A +Version specifies the protocol version number. This is currently 0. A message with unknown version number MUST be ignored. The connection where such a packet came from MAY be closed. +Type is one of the message types specified below. + Hop Limit SHOULD be set to `MAX_HOP_COUNT` on message creation, and MUST NOT be changed by a forwarding node. @@ -102,9 +100,6 @@ message. Target Address is the address of the node that should receive the message. -Sequence number is the sequence number of either the source or target -node for this message, depending on type. - ### Encryption Data @@ -203,7 +198,7 @@ A simple chat message. 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Reserved | + | Time | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Text Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -212,4 +207,6 @@ A simple chat message. / / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +Time is the unix timestamp of message sending. + Text the string to be transferred, encoded as UTF-8. diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageHeaderTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageHeaderTest.scala index e7a98a6..d635988 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageHeaderTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageHeaderTest.scala @@ -10,19 +10,15 @@ import com.nutomic.ensichat.protocol.messages.MessageHeaderTest._ object MessageHeaderTest { val h1 = new MessageHeader(Text.Type, MessageHeader.DefaultHopLimit, AddressTest.a1, - AddressTest.a2, 1234, 0, new GregorianCalendar(1970, 1, 1).getTime, 567, 8) + AddressTest.a2, 1234, 0) - val h2 = new MessageHeader(Text.Type, 0, AddressTest.a1, AddressTest.a3, 8765, 234, - new GregorianCalendar(2014, 6, 10).getTime, 0, 0xff) + val h2 = new MessageHeader(Text.Type, 0, AddressTest.a1, AddressTest.a3, 8765, 234) - val h3 = new MessageHeader(Text.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56, - new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0) + val h3 = new MessageHeader(Text.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56) - val h4 = new MessageHeader(0xfff, 0, Address.Null, Address.Broadcast, 0, 0xff, - new GregorianCalendar(1990, 1, 1).getTime, 0, 0xff) + val h4 = new MessageHeader(0xfff, 0, Address.Null, Address.Broadcast, 0, 0xff) - val h5 = new MessageHeader(ConnectionInfo.Type, 0xff, Address.Broadcast, Address.Null, 0xffff, 0, - new GregorianCalendar(2035, 12, 31).getTime, 0xffff, 0) + val h5 = new MessageHeader(ConnectionInfo.Type, 0xff, Address.Broadcast, Address.Null, 0xffff, 0) val headers = Set(h1, h2, h3, h4, h5) diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageTest.scala index 602339b..02ba68f 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/MessageTest.scala @@ -13,11 +13,11 @@ import scala.collection.immutable.TreeSet object MessageTest { - val m1 = new Message(h1, new Text("first")) + val m1 = new Message(h1, new Text("first", new GregorianCalendar(1970, 1, 1).getTime)) - val m2 = new Message(h2, new Text("second")) + val m2 = new Message(h2, new Text("second", new GregorianCalendar(2014, 6, 10).getTime)) - val m3 = new Message(h3, new Text("third")) + val m3 = new Message(h3, new Text("third", new GregorianCalendar(2020, 11, 11).getTime)) val messages = Set(m1, m2, m3) @@ -47,8 +47,7 @@ class MessageTest extends AndroidTestCase { } def testSerializeSigned(): Unit = { - val header = new MessageHeader(ConnectionInfo.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56, - new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0) + val header = new MessageHeader(ConnectionInfo.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56) val m = new Message(header, ConnectionInfoTest.generateCi(getContext)) val signed = Crypto.sign(m) diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala index fa278d6..b547c33 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Message.scala @@ -8,7 +8,10 @@ object Message { * Orders messages by date, oldest messages first. */ val Ordering = new Ordering[Message] { - override def compare(m1: Message, m2: Message) = m1.Header.Time.compareTo(m2.Header.Time) + override def compare(m1: Message, m2: Message) = (m1.Body, m2.Body) match { + case (t1: Text, t2: Text) => t1.time.compareTo(t2.time) + case _ => 0 + } } def read(stream: InputStream): Message = { diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageHeader.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageHeader.scala index 455d25b..b5f271c 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageHeader.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageHeader.scala @@ -7,7 +7,7 @@ import com.nutomic.ensichat.protocol.{Address, BufferUtils} object MessageHeader { - val Length = 20 + 2 * Address.Length + val Length = 12 + 2 * Address.Length val DefaultHopLimit = 20 @@ -31,14 +31,10 @@ object MessageHeader { val hopCount = BufferUtils.getUnsignedByte(b) val length = BufferUtils.getUnsignedInt(b) - val time = new Date(b.getInt().toLong * 1000) val origin = new Address(BufferUtils.getByteArray(b, Address.Length)) val target = new Address(BufferUtils.getByteArray(b, Address.Length)) - val seqNum = BufferUtils.getUnsignedShort(b) - val metric = BufferUtils.getUnsignedByte(b) - - new MessageHeader(messageType, hopLimit, origin, target, seqNum, metric, time, length, hopCount) + new MessageHeader(messageType, hopLimit, origin, target, length, hopCount) } } @@ -50,9 +46,6 @@ case class MessageHeader(MessageType: Int, HopLimit: Int, Origin: Address, Target: Address, - SequenceNumber: Int, - Metric: Int, - Time: Date = new Date(), Length: Long = -1, HopCount: Int = 0) { @@ -68,13 +61,10 @@ case class MessageHeader(MessageType: Int, BufferUtils.putUnsignedByte(b, HopCount) BufferUtils.putUnsignedInt(b, MessageHeader.Length + contentLength) - b.putInt((Time.getTime / 1000).toInt) b.put(Origin.Bytes) b.put(Target.Bytes) - BufferUtils.putUnsignedShort(b, SequenceNumber) - BufferUtils.putUnsignedByte(b, Metric) - BufferUtils.putUnsignedByte(b, 0) + BufferUtils.putUnsignedInt(b, 0) b.array() } @@ -83,11 +73,8 @@ case class MessageHeader(MessageType: Int, case o: MessageHeader => MessageType == o.MessageType && HopLimit == o.HopLimit && - Time.getTime / 1000 == o.Time.getTime / 1000 && Origin == o.Origin && Target == o.Target && - SequenceNumber == o.SequenceNumber && - Metric == o.Metric && HopCount == o.HopCount // Don't compare length as it may be unknown (when header was just created without a body). case _ => false diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala index 777bc18..70ec214 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/Text.scala @@ -1,6 +1,7 @@ package com.nutomic.ensichat.protocol.messages import java.nio.ByteBuffer +import java.util.Date import com.nutomic.ensichat.protocol.BufferUtils @@ -15,10 +16,11 @@ object Text { */ def read(array: Array[Byte]): Text = { val b = ByteBuffer.wrap(array) + val time = new Date(BufferUtils.getUnsignedInt(b) * 1000) val length = BufferUtils.getUnsignedInt(b).toInt val bytes = new Array[Byte](length) b.get(bytes, 0, length) - new Text(new String(bytes, Text.Charset)) + new Text(new String(bytes, Text.Charset), time) } } @@ -26,18 +28,24 @@ object Text { /** * Holds a plain text message. */ -case class Text(text: String) extends MessageBody { +case class Text(text: String, time: Date = new Date()) extends MessageBody { override def Type = Text.Type override def write: Array[Byte] = { val bytes = text.getBytes(Text.Charset) - val b = ByteBuffer.allocate(4 + bytes.length) + val b = ByteBuffer.allocate(length) + BufferUtils.putUnsignedInt(b, time.getTime / 1000) BufferUtils.putUnsignedInt(b, bytes.length) b.put(bytes) b.array() } - override def length = write.length + override def length = 8 + text.getBytes(Text.Charset).length + + override def equals(a: Any): Boolean = a match { + case o: Text => text == text && time.getTime / 1000 == o.time.getTime / 1000 + case _ => false + } } diff --git a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala b/app/src/main/scala/com/nutomic/ensichat/util/Database.scala index fbd9acf..8fcec12 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/Database.scala @@ -58,9 +58,9 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data new Address(c.getString(c.getColumnIndex("origin"))), new Address(c.getString(c.getColumnIndex("target"))), -1, - -1, + -1) + val body = new Text(new String(c.getString(c.getColumnIndex ("text"))), new Date(c.getLong(c.getColumnIndex("date")))) - val body = new Text(new String(c.getString(c.getColumnIndex ("text")))) messages += new Message(header, body) } c.close() @@ -71,13 +71,13 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data * Inserts the given new message into the database. */ def addMessage(message: Message): Unit = message.Body match { - case msg: Text => + case text: Text => val cv = new ContentValues() cv.put("origin", message.Header.Origin.toString) cv.put("target", message.Header.Target.toString) // toString used as workaround for compile error with Long. - cv.put("date", message.Header.Time.getTime.toString) - cv.put("text", msg.text) + cv.put("date", text.time.getTime.toString) + cv.put("text", text.text) getWritableDatabase.insert("messages", null, cv) case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact => // Never stored.