From 5c102581cecca4649e6fb739a58d6daafd8d8746 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Thu, 16 Apr 2015 00:17:10 +0200 Subject: [PATCH] Added unique id to messages, allow extra header fields. --- PROTOCOL.md | 62 ++++++++++----- .../ensichat/protocol/CryptoTest.scala | 8 +- .../ensichat/protocol/RouterTest.scala | 13 ++-- .../protocol/messages/ContentHeaderTest.scala | 42 +++++++++++ .../protocol/messages/MessageHeaderTest.scala | 19 ++--- .../protocol/messages/MessageTest.scala | 10 +-- .../util/AddContactsHandlerTest.scala | 6 +- .../nutomic/ensichat/util/DatabaseTest.scala | 26 +++++-- .../activities/AddContactsActivity.scala | 2 +- .../ConfirmAddContactActivity.scala | 8 +- .../bluetooth/BluetoothInterface.scala | 2 +- .../ensichat/bluetooth/TransferThread.scala | 4 +- .../ensichat/fragments/ChatFragment.scala | 4 +- .../ensichat/protocol/BufferUtils.scala | 2 +- .../ensichat/protocol/ChatService.scala | 26 ++++--- .../nutomic/ensichat/protocol/Crypto.scala | 30 ++++---- .../nutomic/ensichat/protocol/Router.scala | 12 +-- .../ensichat/protocol/SeqNumGenerator.scala | 6 +- .../protocol/messages/AbstractHeader.scala | 68 +++++++++++++++++ .../protocol/messages/ConnectionInfo.scala | 4 +- .../protocol/messages/ContentHeader.scala | 75 +++++++++++++++++++ .../protocol/messages/CryptoData.scala | 1 - .../protocol/messages/EncryptedBody.scala | 4 +- .../ensichat/protocol/messages/Message.scala | 48 ++++++++---- .../protocol/messages/MessageBody.scala | 4 +- .../protocol/messages/MessageHeader.scala | 74 +++++------------- .../protocol/messages/RequestAddContact.scala | 4 +- .../protocol/messages/ResultAddContact.scala | 4 +- .../ensichat/protocol/messages/Text.scala | 4 +- .../ensichat/protocol/messages/UserName.scala | 4 +- .../ensichat/util/AddContactsHandler.scala | 22 +++--- .../com/nutomic/ensichat/util/Database.scala | 26 +++---- .../ensichat/util/MessagesAdapter.scala | 4 +- .../ensichat/util/NotificationHandler.scala | 8 +- 34 files changed, 428 insertions(+), 208 deletions(-) create mode 100644 app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/ContentHeaderTest.scala create mode 100644 app/src/main/scala/com/nutomic/ensichat/protocol/messages/AbstractHeader.scala create mode 100644 app/src/main/scala/com/nutomic/ensichat/protocol/messages/ContentHeader.scala diff --git a/PROTOCOL.md b/PROTOCOL.md index 86fbe7d..f91cce6 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -29,15 +29,15 @@ either address. Messages -------- -All messages are signed using RSASSA-PKCS1-v1_5. All messages except -ConnectionInfo are encrypted using AES/CBC/PKCS5Padding, after which -the AES key is wrapped with the recipient's public RSA key. +All messages are signed using RSASSA-PKCS1-v1_5. All Content Messages +except are encrypted using AES/CBC/PKCS5Padding, after which the +AES key is wrapped with the recipient's public RSA key. 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / / - \ Header (76 bytes) \ + \ Header (74 or 80 bytes) \ / / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / / @@ -52,14 +52,15 @@ the AES key is wrapped with the recipient's public RSA key. ### Header -Every message starts with one 76 byte header indicating the message +Every message starts with one 74 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. +header is in network byte order, i.e. big endian. The header may have +6 bytes of additional data. 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Version | Type | Hop Limit | Hop Count | + | Version | Protocol-Type | Hop Limit | Hop Count | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -71,14 +72,17 @@ header is in network byte order, i.e. big endian. | Target Address (32 bytes) | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sequence Number | Reserved | + | Sequence Number | Content-Type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Message ID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 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. +Protocol-Type is one of those specified in section Protocol Messages, +or 255 for Content Messages. Hop Limit SHOULD be set to `MAX_HOP_COUNT` on message creation, and MUST NOT be changed by a forwarding node. @@ -105,6 +109,13 @@ each new message sent (after 2^16-1 comes 0 again). It SHOULD be persistent during restarts. It is used by intermediate nodes to avoid forwarding the same message multiple times. +Content-Type is one of those in section Content-Messages. + +Message ID is unique for each message by the same sender. A device MUST NOT +ever send two messages with the same Message ID. + +Only Content Messages have the Content-Type and Message ID +fields. ### Encryption Data @@ -129,14 +140,22 @@ Signature is the cryptographic signature over the (unencrypted) message header and message body. -ConnectionInfo (Type = 0) ---------- +Protocol Messages +----------------- + +These messages are sent by the protocol, without any user interaction. +They are not encrypted, and do not contain the Content-Type and +Message ID fields. + + +### ConnectionInfo (Protocol-Type = 1) After successfully connecting to a node via Bluetooth, public keys are exchanged. Each node MUST send this as the first message over the connection. Hop Limit MUST be 1 for this message type (i.e. it -must never be forwarded). Origin Address and Target Address MUST be -set to all zeros, and MUST be ignored by the receiving node. +must never be forwarded). Origin Address, Target Address and Sequence +Number MUST be set to all zeros, and MUST be ignored by the receiving +node. A receiving node SHOULD store the key in permanent storage if it hasn't already stored it earlier. However, a node MAY decide to @@ -167,7 +186,16 @@ After this message has been received, communication with normal messages may start. -### RequestAddContact (Type = 4) +Content Messages +---------------- + +These messages are initiated by user action. They are encrypted, and +contain the Content-Type and Message ID fields. + +These messages always have a Protocol-Type of 255. + + +### RequestAddContact (Content-Type = 1) Sent when a user wants to add another node as a contact. After this, a ResultAddContact message should be returned. @@ -179,7 +207,7 @@ a ResultAddContact message should be returned. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -### ResultAddContact (Type = 5) +### ResultAddContact (Content-Type = 2) Sent as response to a RequestAddContact message. @@ -194,7 +222,7 @@ otherwise. Nodes should only add another node as a contact if both users agreed. -### Text (Type = 6) +### Text (Content-Type = 3) A simple chat message. @@ -214,7 +242,7 @@ Time is the unix timestamp of message sending. Text the string to be transferred, encoded as UTF-8. -### UserName (Type = 7) +### UserName (Content-Type = 4) 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 diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/CryptoTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/CryptoTest.scala index ec27563..7531843 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/CryptoTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/CryptoTest.scala @@ -19,8 +19,8 @@ class CryptoTest extends AndroidTestCase { MessageTest.messages.foreach { m => val signed = crypto.sign(m) assertTrue(crypto.verify(signed, crypto.getLocalPublicKey)) - assertEquals(m.Header, signed.Header) - assertEquals(m.Body, signed.Body) + assertEquals(m.header, signed.header) + assertEquals(m.body, signed.body) } } @@ -28,8 +28,8 @@ class CryptoTest extends AndroidTestCase { MessageTest.messages.foreach{ m => val encrypted = crypto.encrypt(crypto.sign(m), crypto.getLocalPublicKey) val decrypted = crypto.decrypt(encrypted) - assertEquals(m.Body, decrypted.Body) - assertEquals(m.Header, encrypted.Header) + assertEquals(m.body, decrypted.body) + assertEquals(m.header, encrypted.header) } } diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/RouterTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/RouterTest.scala index d23824a..b0e388f 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/RouterTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/RouterTest.scala @@ -1,7 +1,6 @@ package com.nutomic.ensichat.protocol import android.test.AndroidTestCase -import android.util.Log import com.nutomic.ensichat.protocol.messages._ import junit.framework.Assert._ @@ -68,14 +67,14 @@ class RouterTest extends AndroidTestCase { assertEquals(neighbors(), sentTo) } - test(1, MessageHeader.SeqNumRange.last) - test(MessageHeader.SeqNumRange.last / 2, MessageHeader.SeqNumRange.last) - test(MessageHeader.SeqNumRange.last / 2, 1) + test(1, ContentHeader.SeqNumRange.last) + test(ContentHeader.SeqNumRange.last / 2, ContentHeader.SeqNumRange.last) + test(ContentHeader.SeqNumRange.last / 2, 1) } private def generateMessage(sender: Address, receiver: Address, seqNum: Int): Message = { - val header = new MessageHeader(0, MessageHeader.DefaultHopLimit, sender, receiver, seqNum) - new Message(header, new CryptoData(None, None), new UserName("")) + val header = new ContentHeader(sender, receiver, seqNum, UserName.Type, 5) + new Message(header, new UserName("")) } -} \ No newline at end of file +} diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/ContentHeaderTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/ContentHeaderTest.scala new file mode 100644 index 0000000..59977b4 --- /dev/null +++ b/app/src/androidTest/scala/com/nutomic/ensichat/protocol/messages/ContentHeaderTest.scala @@ -0,0 +1,42 @@ +package com.nutomic.ensichat.protocol.messages + +import android.test.AndroidTestCase +import com.nutomic.ensichat.protocol.{Address, AddressTest} +import junit.framework.Assert._ + +object ContentHeaderTest { + + val h1 = new ContentHeader(AddressTest.a1, AddressTest.a2, 1234, + Text.Type, 123, 5) + + val h2 = new ContentHeader(AddressTest.a1, AddressTest.a3, + 30000, Text.Type, 8765, 20) + + val h3 = new ContentHeader(AddressTest.a4, AddressTest.a2, + 250, Text.Type, 77, 123) + + val h4 = new ContentHeader(Address.Null, Address.Broadcast, + ContentHeader.SeqNumRange.last, 0, 0xffff, 0) + + val h5 = new ContentHeader(Address.Broadcast, Address.Null, + 0, 0xff, 0, 0xff) + + val headers = Set(h1, h2, h3, h4, h5) + +} + +class ContentHeaderTest extends AndroidTestCase { + + def testSerialize(): Unit = { + ContentHeaderTest.headers.foreach{h => + val bytes = h.write(0) + assertEquals(bytes.length, h.length) + val (mh, length) = MessageHeader.read(bytes) + val chBytes = bytes.drop(mh.length) + val (header, remaining) = ContentHeader.read(mh, chBytes) + assertEquals(h, header) + assertEquals(0, remaining.length) + } + } + +} 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 793fb19..6b5f909 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 @@ -7,18 +7,15 @@ import junit.framework.Assert._ object MessageHeaderTest { - val h1 = new MessageHeader(Text.Type, MessageHeader.DefaultHopLimit, AddressTest.a1, - AddressTest.a2, 5, 1234, 0) + val h1 = new MessageHeader(ContentHeader.ContentMessageType, AddressTest.a1, AddressTest.a2, 1234, + 0) - val h2 = new MessageHeader(Text.Type, 0, AddressTest.a1, AddressTest.a3, 30000, 8765, 234) + val h2 = new MessageHeader(ContentHeader.ContentMessageType, Address.Null, Address.Broadcast, + ContentHeader.SeqNumRange.last, 0xff) - val h3 = new MessageHeader(Text.Type, 0xff, AddressTest.a4, AddressTest.a2, 250, 0, 56) + val h3 = new MessageHeader(ContentHeader.ContentMessageType, Address.Broadcast, Address.Null, 0) - val h4 = new MessageHeader(0xff, 0, Address.Null, Address.Broadcast, MessageHeader.SeqNumRange.last, 0, 0xff) - - val h5 = new MessageHeader(ConnectionInfo.Type, 0xff, Address.Broadcast, Address.Null, 0, 0xffff, 0) - - val headers = Set(h1, h2, h3, h4, h5) + val headers = Set(h1, h2, h3) } @@ -27,9 +24,9 @@ class MessageHeaderTest extends AndroidTestCase { def testSerialize(): Unit = { headers.foreach{h => val bytes = h.write(0) - val header = MessageHeader.read(bytes) + val (header, length) = MessageHeader.read(bytes) assertEquals(h, header) - assertEquals(bytes.length, header.length) + assertEquals(MessageHeader.Length, length) } } 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 010e1ed..c696c48 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 @@ -4,7 +4,7 @@ import java.io.ByteArrayInputStream import java.util.GregorianCalendar import android.test.AndroidTestCase -import com.nutomic.ensichat.protocol.messages.MessageHeaderTest._ +import com.nutomic.ensichat.protocol.messages.ContentHeaderTest._ import com.nutomic.ensichat.protocol.messages.MessageTest._ import com.nutomic.ensichat.protocol.{AddressTest, Crypto} import junit.framework.Assert._ @@ -47,7 +47,7 @@ class MessageTest extends AndroidTestCase { } def testSerializeSigned(): Unit = { - val header = new MessageHeader(ConnectionInfo.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56) + val header = new MessageHeader(ConnectionInfo.Type, AddressTest.a4, AddressTest.a2, 0) val m = new Message(header, ConnectionInfoTest.generateCi(getContext)) val signed = crypto.sign(m) @@ -65,10 +65,10 @@ class MessageTest extends AndroidTestCase { val bytes = encrypted.write val read = Message.read(new ByteArrayInputStream(bytes)) - assertEquals(encrypted.Crypto, read.Crypto) + assertEquals(encrypted.crypto, read.crypto) val decrypted = crypto.decrypt(read) - assertEquals(m.Header, decrypted.Header) - assertEquals(m.Body, decrypted.Body) + assertEquals(m.header, decrypted.header) + assertEquals(m.body, decrypted.body) assertTrue(crypto.verify(decrypted, crypto.getLocalPublicKey)) } } diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/util/AddContactsHandlerTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/util/AddContactsHandlerTest.scala index 2e5165b..92efac3 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/util/AddContactsHandlerTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/util/AddContactsHandlerTest.scala @@ -24,11 +24,11 @@ class AddContactsHandlerTest extends AndroidTestCase { private lazy val crypto = new Crypto(getContext) private lazy val header1 = - new MessageHeader(RequestAddContact.Type, 0, UserTest.u1.address, crypto.localAddress, 0) + new ContentHeader(UserTest.u1.address, crypto.localAddress, 0, RequestAddContact.Type, 0) private lazy val header2 = - new MessageHeader(ResultAddContact.Type, 0, UserTest.u1.address, crypto.localAddress, 0) + new ContentHeader(UserTest.u1.address, crypto.localAddress, 0, ResultAddContact.Type, 0) private lazy val header3 = - new MessageHeader(ResultAddContact.Type, 0, crypto.localAddress, UserTest.u1.address, 0) + new ContentHeader(crypto.localAddress, UserTest.u1.address, 0, ResultAddContact.Type, 0) override def tearDown(): Unit = { super.tearDown() diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala index 92da3b6..8a44025 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala @@ -5,10 +5,11 @@ import java.util.concurrent.CountDownLatch import android.content.Context import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase -import android.test.AndroidTestCase -import android.test.mock +import android.test.{AndroidTestCase, mock} import com.nutomic.ensichat.protocol.UserTest +import com.nutomic.ensichat.protocol.messages.ContentHeaderTest._ import com.nutomic.ensichat.protocol.messages.MessageTest._ +import com.nutomic.ensichat.protocol.messages.{ContentHeader, CryptoData} import com.nutomic.ensichat.util.Database.OnContactsUpdatedListener import junit.framework.Assert._ @@ -50,24 +51,37 @@ class DatabaseTest extends AndroidTestCase { } def testMessageCount(): Unit = { - val msg1 = database.getMessages(m1.Header.origin, 1) + val msg1 = database.getMessages(m1.header.origin, 1) assertEquals(1, msg1.size) - val msg2 = database.getMessages(m1.Header.origin, 3) + val msg2 = database.getMessages(m1.header.origin, 3) assertEquals(2, msg2.size) } def testMessageOrder(): Unit = { - val msg = database.getMessages(m1.Header.target, 1) + val msg = database.getMessages(m1.header.target, 1) assertTrue(msg.contains(m3)) } def testMessageSelect(): Unit = { - val msg = database.getMessages(m1.Header.target, 2) + val msg = database.getMessages(m1.header.target, 2) assertTrue(msg.contains(m1)) assertTrue(msg.contains(m3)) } + def testMessageFields(): Unit = { + val msg = database.getMessages(m3.header.target, 1).firstKey + val header = msg.header.asInstanceOf[ContentHeader] + + assertEquals(h3.origin, header.origin) + assertEquals(h3.target, header.target) + assertEquals(-1, msg.header.seqNum) + assertEquals(h3.contentType, header.contentType) + assertEquals(h3.messageId, header.messageId) + assertEquals(new CryptoData(None, None), msg.crypto) + assertEquals(m3.body, msg.body) + } + def testAddContact(): Unit = { database.addContact(UserTest.u1) val contacts = database.getContacts diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala index 6986bbe..4dcf49d 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala @@ -7,7 +7,7 @@ import android.view._ import android.widget.AdapterView.OnItemClickListener import android.widget._ import com.nutomic.ensichat.R -import com.nutomic.ensichat.protocol.{User, ChatService} +import com.nutomic.ensichat.protocol.ChatService import com.nutomic.ensichat.protocol.messages.RequestAddContact import com.nutomic.ensichat.util.Database.OnContactsUpdatedListener import com.nutomic.ensichat.util.{Database, UsersAdapter} diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactActivity.scala index 3b00967..2b542a6 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/ConfirmAddContactActivity.scala @@ -4,14 +4,12 @@ import android.app.AlertDialog import android.content.DialogInterface.OnClickListener import android.content.{Context, DialogInterface} import android.os.Bundle -import android.util.Log import android.view.{ContextThemeWrapper, LayoutInflater} -import android.widget.{ImageView, TextView, Toast} +import android.widget.{ImageView, TextView} import com.nutomic.ensichat.R -import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener -import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact} +import com.nutomic.ensichat.protocol.messages.ResultAddContact import com.nutomic.ensichat.protocol.{Address, Crypto} -import com.nutomic.ensichat.util.{Database, IdenticonGenerator} +import com.nutomic.ensichat.util.IdenticonGenerator object ConfirmAddContactActivity { diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala index 3397533..8f47fe9 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala @@ -183,7 +183,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler, * @param msg The message that was received. * @param device Device that sent the message. */ - private def onReceiveMessage(msg: Message, device: Device.ID): Unit = msg.Body match { + private def onReceiveMessage(msg: Message, device: Device.ID): Unit = msg.body match { case info: ConnectionInfo => val address = crypto.calculateAddress(info.key) // Service.onConnectionOpened sends message, so mapping already needs to be in place. diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala index 2a4ff4a..65beddc 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala @@ -44,8 +44,8 @@ class TransferThread(device: Device, socket: BluetoothSocket, Handler: Bluetooth override def run(): Unit = { Log.i(Tag, "Starting data transfer with " + device.toString) - send(Crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit, - Address.Null, Address.Null, 0, 0), new ConnectionInfo(Crypto.getLocalPublicKey)))) + send(Crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, + Address.Null, Address.Null, 0), new ConnectionInfo(Crypto.getLocalPublicKey)))) while (socket.isConnected) { try { diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala b/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala index 3f7e388..181de6a 100644 --- a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala +++ b/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala @@ -111,10 +111,10 @@ class ChatFragment extends ListFragment with OnClickListener * Displays new messages in UI. */ override def onMessageReceived(msg: Message): Unit = { - if (!Set(msg.Header.origin, msg.Header.target).contains(address)) + if (!Set(msg.header.origin, msg.header.target).contains(address)) return - msg.Body match { + msg.body match { case _: Text => adapter.add(msg) case _ => } diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/BufferUtils.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/BufferUtils.scala index 7adadea..4c8ea64 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/BufferUtils.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/BufferUtils.scala @@ -30,4 +30,4 @@ object BufferUtils { def toString(array: Array[Byte]) = array.map("%02X".format(_)).mkString -} \ No newline at end of file +} diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala index abf6381..77ba1f8 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/ChatService.scala @@ -1,18 +1,17 @@ package com.nutomic.ensichat.protocol -import android.app.{Notification, NotificationManager, PendingIntent, Service} +import android.app.Service import android.bluetooth.BluetoothAdapter -import android.content.{Context, Intent} +import android.content.Intent import android.os.Handler import android.preference.PreferenceManager import android.util.Log import com.nutomic.ensichat.R -import com.nutomic.ensichat.activities.{MainActivity, ConfirmAddContactActivity} import com.nutomic.ensichat.bluetooth.BluetoothInterface import com.nutomic.ensichat.fragments.SettingsFragment import com.nutomic.ensichat.protocol.ChatService.{OnConnectionsChangedListener, OnMessageReceivedListener} import com.nutomic.ensichat.protocol.messages._ -import com.nutomic.ensichat.util.{AddContactsHandler, NotificationHandler, Database} +import com.nutomic.ensichat.util.{AddContactsHandler, Database, NotificationHandler} import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global @@ -138,8 +137,11 @@ class ChatService extends Service { if (!btInterface.getConnections.contains(target)) return - val header = new MessageHeader(body.messageType, MessageHeader.DefaultHopLimit, - crypto.localAddress, target, seqNumGenerator.next()) + val sp = PreferenceManager.getDefaultSharedPreferences(this) + val messageId = sp.getLong("message_id", 0) + val header = new ContentHeader(crypto.localAddress, target, seqNumGenerator.next(), + body.contentType, messageId) + sp.edit().putLong("message_id", messageId + 1) val msg = new Message(header, body) val encrypted = crypto.encrypt(crypto.sign(msg)) @@ -154,10 +156,10 @@ class ChatService extends Service { * Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]]. */ def onMessageReceived(msg: Message): Unit = { - if (msg.Header.target == crypto.localAddress) { + if (msg.header.target == crypto.localAddress) { val decrypted = crypto.decrypt(msg) if (!crypto.verify(decrypted)) { - Log.i(Tag, "Ignoring message with invalid signature from " + msg.Header.origin) + Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin) return } onNewMessage(decrypted) @@ -169,11 +171,11 @@ class ChatService extends Service { /** * Handles all (locally and remotely sent) new messages. */ - private def onNewMessage(msg: Message): Unit = msg.Body match { + private def onNewMessage(msg: Message): Unit = msg.body match { case name: UserName => - val contact = new User(msg.Header.origin, name.name) + val contact = new User(msg.header.origin, name.name) knownUsers += contact - if (database.getContact(msg.Header.origin).nonEmpty) + if (database.getContact(msg.header.origin).nonEmpty) database.changeContactName(contact) callConnectionListeners() @@ -204,7 +206,7 @@ class ChatService extends Service { false } - val info = msg.Body.asInstanceOf[ConnectionInfo] + val info = msg.body.asInstanceOf[ConnectionInfo] val sender = crypto.calculateAddress(info.key) if (sender == Address.Broadcast || sender == Address.Null) { Log.i(Tag, "Ignoring ConnectionInfo message with invalid sender " + sender) diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala index d3ebef0..86978fc 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/Crypto.scala @@ -114,7 +114,7 @@ class Crypto(context: Context) { /** * Adds a new public key for a remote device. * - * @throws RuntimeException If a this key + * @throws RuntimeException If a key already exists for this address. */ @throws[RuntimeException] def addPublicKey(address: Address, key: PublicKey): Unit = { @@ -128,18 +128,18 @@ class Crypto(context: Context) { val sig = Signature.getInstance(SignAlgorithm) val key = loadKey(PrivateKeyAlias, classOf[PrivateKey]) sig.initSign(key) - sig.update(msg.Body.write) - new Message(msg.Header, new CryptoData(Option(sig.sign), None), msg.Body) + sig.update(msg.body.write) + new Message(msg.header, new CryptoData(Option(sig.sign), None), msg.body) } def verify(msg: Message, key: PublicKey = null): Boolean = { val publicKey = if (key != null) key - else loadKey(msg.Header.origin.toString, classOf[PublicKey]) + else loadKey(msg.header.origin.toString, classOf[PublicKey]) val sig = Signature.getInstance(SignAlgorithm) sig.initVerify(publicKey) - sig.update(msg.Body.write) - sig.verify(msg.Crypto.signature.get) + sig.update(msg.body.write) + sig.verify(msg.crypto.signature.get) } /** @@ -223,42 +223,42 @@ class Crypto(context: Context) { private def keyFolder = new File(context.getFilesDir, "keys") def encrypt(msg: Message, key: PublicKey = null): Message = { - assert(msg.Crypto.signature.isDefined, "Message must be signed before encryption") + assert(msg.crypto.signature.isDefined, "Message must be signed before encryption") // Symmetric encryption of data val secretKey = makeSecretKey() val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm) symmetricCipher.init(Cipher.ENCRYPT_MODE, secretKey) - val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.Body.write)) + val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.body.write)) // Asymmetric encryption of secret key val publicKey = if (key != null) key - else loadKey(msg.Header.target.toString, classOf[PublicKey]) + else loadKey(msg.header.target.toString, classOf[PublicKey]) val asymmetricCipher = Cipher.getInstance(KeyAlgorithm) asymmetricCipher.init(Cipher.WRAP_MODE, publicKey) - new Message(msg.Header, - new CryptoData(msg.Crypto.signature, Option(asymmetricCipher.wrap(secretKey))), encrypted) + new Message(msg.header, + new CryptoData(msg.crypto.signature, Option(asymmetricCipher.wrap(secretKey))), encrypted) } def decrypt(msg: Message): Message = { // Asymmetric decryption of secret key val asymmetricCipher = Cipher.getInstance(KeyAlgorithm) asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey])) - val key = asymmetricCipher.unwrap(msg.Crypto.key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY) + val key = asymmetricCipher.unwrap(msg.crypto.key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY) // Symmetric decryption of data val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm) symmetricCipher.init(Cipher.DECRYPT_MODE, key) - val decrypted = copyThroughCipher(symmetricCipher, msg.Body.asInstanceOf[EncryptedBody].data) - val body = msg.Header.messageType match { + val decrypted = copyThroughCipher(symmetricCipher, msg.body.asInstanceOf[EncryptedBody].data) + val body = msg.header.asInstanceOf[ContentHeader].contentType match { case RequestAddContact.Type => RequestAddContact.read(decrypted) case ResultAddContact.Type => ResultAddContact.read(decrypted) case Text.Type => Text.read(decrypted) case UserName.Type => UserName.read(decrypted) } - new Message(msg.Header, msg.Crypto, body) + new Message(msg.header, msg.crypto, body) } /** diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/Router.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/Router.scala index a9e40b5..60893b1 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/Router.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/Router.scala @@ -1,6 +1,6 @@ package com.nutomic.ensichat.protocol -import com.nutomic.ensichat.protocol.messages.{Message, MessageHeader} +import com.nutomic.ensichat.protocol.messages.{ContentHeader, Message} /** * Forwards messages to all connected devices. @@ -10,7 +10,7 @@ class Router(activeConnections: () => Set[Address], send: (Address, Message) => private var messageSeen = Set[(Address, Int)]() def onReceive(msg: Message): Unit = { - val info = (msg.Header.origin, msg.Header.seqNum) + val info = (msg.header.origin, msg.header.seqNum) if (messageSeen.contains(info)) return @@ -34,14 +34,14 @@ class Router(activeConnections: () => Set[Address], send: (Address, Message) => // True if [[s2]] is between {{{MessageHeader.SeqNumRange.size / 2}}} and // [[MessageHeader.SeqNumRange.size]]. - if (s1 > MessageHeader.SeqNumRange.size / 2) { + if (s1 > ContentHeader.SeqNumRange.size / 2) { // True if [[s2]] is between {{{s1 - MessageHeader.SeqNumRange.size / 2}}} and [[s1]]. - s1 - MessageHeader.SeqNumRange.size / 2 < s2 && s2 < s1 + s1 - ContentHeader.SeqNumRange.size / 2 < s2 && s2 < s1 } else { // True if [[s2]] is *not* between [[s1]] and {{{s1 + MessageHeader.SeqNumRange.size / 2}}}. - s2 < s1 || s2 > s1 + MessageHeader.SeqNumRange.size / 2 + s2 < s1 || s2 > s1 + ContentHeader.SeqNumRange.size / 2 } } } -} \ No newline at end of file +} diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/SeqNumGenerator.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/SeqNumGenerator.scala index f7396ed..431af20 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/SeqNumGenerator.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/SeqNumGenerator.scala @@ -2,7 +2,7 @@ package com.nutomic.ensichat.protocol import android.content.Context import android.preference.PreferenceManager -import com.nutomic.ensichat.protocol.messages.MessageHeader +import com.nutomic.ensichat.protocol.messages.ContentHeader /** * Generates sequence numbers acorrding to protocol, which are stored persistently. @@ -13,7 +13,7 @@ class SeqNumGenerator(context: Context) { private val pm = PreferenceManager.getDefaultSharedPreferences(context) - private var current = pm.getInt(KeySequenceNumber, MessageHeader.SeqNumRange.head) + private var current = pm.getInt(KeySequenceNumber, ContentHeader.SeqNumRange.head) def next(): Int = { current += 1 @@ -21,4 +21,4 @@ class SeqNumGenerator(context: Context) { current } -} \ No newline at end of file +} diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/AbstractHeader.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/AbstractHeader.scala new file mode 100644 index 0000000..e9e89b0 --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/AbstractHeader.scala @@ -0,0 +1,68 @@ +package com.nutomic.ensichat.protocol.messages + +import java.nio.ByteBuffer + +import com.nutomic.ensichat.protocol.{Address, BufferUtils} + +object AbstractHeader { + + val DefaultHopLimit = 20 + + val Version = 0 + + private[messages] val Length = 10 + 2 * Address.Length + +} + +/** + * Contains the header fields and functionality that are used both in [[MessageHeader]] and + * [[ContentHeader]]. + */ +trait AbstractHeader { + + def protocolType: Int + def hopLimit: Int + def hopCount: Int + def origin: Address + def target: Address + def seqNum: Int + + /** + * Writes the header to byte array. + */ + def write(contentLength: Int): Array[Byte] = { + val b = ByteBuffer.allocate(AbstractHeader.Length) + + BufferUtils.putUnsignedByte(b, AbstractHeader.Version) + BufferUtils.putUnsignedByte(b, protocolType) + BufferUtils.putUnsignedByte(b, hopLimit) + BufferUtils.putUnsignedByte(b, hopCount) + + BufferUtils.putUnsignedInt(b, length + contentLength) + b.put(origin.bytes) + b.put(target.bytes) + + BufferUtils.putUnsignedShort(b, seqNum) + + b.array() + } + + /** + * Returns true if this object is an instance of [[ContentHeader]]. + */ + def isContentMessage = protocolType == ContentHeader.ContentMessageType + + def length: Int + + override def equals(a: Any): Boolean = a match { + case o: AbstractHeader => + protocolType == o.protocolType && + hopLimit == o.hopLimit && + hopCount == o.hopCount && + origin == o.origin && + target == o.target && + seqNum == o.seqNum + case _ => false + } + +} diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ConnectionInfo.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ConnectionInfo.scala index 13037c1..a5d8c72 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ConnectionInfo.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ConnectionInfo.scala @@ -33,7 +33,9 @@ object ConnectionInfo { */ case class ConnectionInfo(key: PublicKey) extends MessageBody { - override def messageType = ConnectionInfo.Type + override def protocolType = ConnectionInfo.Type + + override def contentType = -1 override def write: Array[Byte] = { val b = ByteBuffer.allocate(length) diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ContentHeader.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ContentHeader.scala new file mode 100644 index 0000000..900175a --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ContentHeader.scala @@ -0,0 +1,75 @@ +package com.nutomic.ensichat.protocol.messages + +import java.nio.ByteBuffer + +import com.nutomic.ensichat.protocol.{Address, BufferUtils} + +object ContentHeader { + + val Length = 6 + + val ContentMessageType = 255 + + val SeqNumRange = 0 until 1 << 16 + + /** + * Constructs [[MessageHeader]] from byte array. + */ + def read(mh: AbstractHeader, bytes: Array[Byte]): (ContentHeader, Array[Byte]) = { + val b = ByteBuffer.wrap(bytes) + + val contentType = BufferUtils.getUnsignedShort(b) + val messageId = BufferUtils.getUnsignedInt(b) + + val ch = new ContentHeader(mh.origin, mh.target, + mh.seqNum, contentType, messageId, mh.hopCount) + + val remaining = new Array[Byte](b.remaining()) + b.get(remaining, 0, b.remaining()) + (ch, remaining) + } + +} + +/** + * Header for user-sent messages. + * + * This is [[AbstractHeader]] with some extra data appended. + */ +case class ContentHeader(override val origin: Address, + override val target: Address, + override val seqNum: Int, + contentType: Int, + messageId: Long, + override val hopCount: Int = 0) + extends AbstractHeader { + + override val protocolType = ContentHeader.ContentMessageType + + override val hopLimit = AbstractHeader.DefaultHopLimit + + /** + * Writes the header to byte array. + */ + override def write(contentLength: Int): Array[Byte] = { + val b = ByteBuffer.allocate(length) + + b.put(super.write(contentLength)) + + BufferUtils.putUnsignedShort(b, contentType) + BufferUtils.putUnsignedInt(b, messageId) + + b.array() + } + + override def length = AbstractHeader.Length + ContentHeader.Length + + override def equals(a: Any): Boolean = a match { + case o: ContentHeader => + super.equals(a) && + contentType == o.contentType && + messageId == o.messageId + case _ => false + } + +} diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/CryptoData.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/CryptoData.scala index bb5e5d9..50cff63 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/CryptoData.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/CryptoData.scala @@ -28,7 +28,6 @@ object CryptoData { val remaining = new Array[Byte](b.remaining()) b.get(remaining, 0, b.remaining()) (new CryptoData(Some(signature), key), remaining) - } } diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/EncryptedBody.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/EncryptedBody.scala index 660097e..077ed35 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/EncryptedBody.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/EncryptedBody.scala @@ -5,7 +5,9 @@ package com.nutomic.ensichat.protocol.messages */ case class EncryptedBody(data: Array[Byte]) extends MessageBody { - override def messageType = -1 + override def protocolType = -1 + + override def contentType = -1 def write = data 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 0a53a6b..8ef9fef 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 @@ -9,36 +9,43 @@ object Message { * Orders messages by date, oldest messages first. */ val Ordering = new Ordering[Message] { - override def compare(m1: Message, m2: Message) = (m1.Body, m2.Body) match { + override def compare(m1: Message, m2: Message) = (m1.body, m2.body) match { case (t1: Text, t2: Text) => t1.time.compareTo(t2.time) case _ => 0 } } + class ParseMessageException(detailMessage: String) extends RuntimeException(detailMessage) { + } + val Charset = "UTF-8" class ReadMessageException(throwable: Throwable) extends RuntimeException(throwable) + /** + * Reads the entire message (header, crypto and body) into an object. + */ def read(stream: InputStream): Message = { try { val headerBytes = new Array[Byte](MessageHeader.Length) stream.read(headerBytes, 0, MessageHeader.Length) - val header = MessageHeader.read(headerBytes) + var (header: AbstractHeader, length) = MessageHeader.read(headerBytes) - val contentLength = (header.length - MessageHeader.Length).toInt - val contentBytes = new Array[Byte](contentLength) - var numRead = 0 - do { - numRead += stream.read(contentBytes, numRead, contentLength - numRead) - } while (numRead < contentLength) + var contentBytes = readStream(stream, length - header.length) + + if (header.isContentMessage) { + val ret: (ContentHeader, Array[Byte]) = ContentHeader.read(header, contentBytes) + header = ret._1 + contentBytes = ret._2 + } val (crypto, remaining) = CryptoData.read(contentBytes) val body = - header.messageType match { + header.protocolType match { case ConnectionInfo.Type => ConnectionInfo.read(remaining) - case _ => new EncryptedBody(remaining) + case _ => new EncryptedBody(remaining) } new Message(header, crypto, body) @@ -48,13 +55,26 @@ object Message { } } + /** + * Reads length bytes from stream and returns them. + */ + private def readStream(stream: InputStream, length: Int): Array[Byte] = { + val contentBytes = new Array[Byte](length) + + var numRead = 0 + do { + numRead += stream.read(contentBytes, numRead, length - numRead) + } while (numRead < length) + contentBytes + } + } -case class Message(Header: MessageHeader, Crypto: CryptoData, Body: MessageBody) { +case class Message(header: AbstractHeader, crypto: CryptoData, body: MessageBody) { - def this(header: MessageHeader, body: MessageBody) = + def this(header: AbstractHeader, body: MessageBody) = this(header, new CryptoData(None, None), body) - def write = Header.write(Body.length + Crypto.length) ++ Crypto.write ++ Body.write + def write = header.write(body.length + crypto.length) ++ crypto.write ++ body.write -} \ No newline at end of file +} diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageBody.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageBody.scala index b1fe5c3..c2acc13 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageBody.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/MessageBody.scala @@ -5,7 +5,9 @@ package com.nutomic.ensichat.protocol.messages */ abstract class MessageBody { - def messageType: Int + def protocolType: Int + + def contentType: Int /** * Writes the message contents to a byte array. 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 7479cf3..49e52a9 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 @@ -2,31 +2,25 @@ package com.nutomic.ensichat.protocol.messages import java.nio.ByteBuffer +import com.nutomic.ensichat.protocol.messages.Message.ParseMessageException import com.nutomic.ensichat.protocol.{Address, BufferUtils} object MessageHeader { - val Length = 12 + 2 * Address.Length - - val DefaultHopLimit = 20 - - val Version = 0 - - val SeqNumRange = 0 until ((2 << 16) - 1) - - class ParseMessageException(detailMessage: String) extends RuntimeException(detailMessage) { - } + val Length = AbstractHeader.Length /** - * Constructs [[MessageHeader]] from byte array. + * Constructs header from byte array. + * + * @return The header and the message length in bytes. */ - def read(bytes: Array[Byte]): MessageHeader = { - val b = ByteBuffer.wrap(bytes, 0, Length) + def read(bytes: Array[Byte]): (MessageHeader, Int) = { + val b = ByteBuffer.wrap(bytes, 0, MessageHeader.Length) val version = BufferUtils.getUnsignedByte(b) - if (version != Version) + if (version != AbstractHeader.Version) throw new ParseMessageException("Failed to parse message with unsupported version " + version) - val messageType = BufferUtils.getUnsignedByte(b) + val protocolType = BufferUtils.getUnsignedByte(b) val hopLimit = BufferUtils.getUnsignedByte(b) val hopCount = BufferUtils.getUnsignedByte(b) @@ -36,52 +30,24 @@ object MessageHeader { val seqNum = BufferUtils.getUnsignedShort(b) - new MessageHeader(messageType, hopLimit, origin, target, seqNum, length, hopCount) + (new MessageHeader(protocolType, origin, target, seqNum, hopCount, hopLimit), length.toInt) } } /** * First part of any message, used for routing. + * + * This is the same as [[AbstractHeader]]. */ -case class MessageHeader(messageType: Int, - hopLimit: Int, - origin: Address, - target: Address, - seqNum: Int, - length: Long = -1, - hopCount: Int = 0) { +case class MessageHeader(override val protocolType: Int, + override val origin: Address, + override val target: Address, + override val seqNum: Int, + override val hopCount: Int = 0, + override val hopLimit: Int = AbstractHeader.DefaultHopLimit) + extends AbstractHeader { - /** - * Writes the header to byte array. - */ - def write(contentLength: Int): Array[Byte] = { - val b = ByteBuffer.allocate(MessageHeader.Length) - - BufferUtils.putUnsignedByte(b, MessageHeader.Version) - BufferUtils.putUnsignedByte(b, messageType) - BufferUtils.putUnsignedByte(b, hopLimit) - BufferUtils.putUnsignedByte(b, hopCount) - - BufferUtils.putUnsignedInt(b, MessageHeader.Length + contentLength) - b.put(origin.bytes) - b.put(target.bytes) - - BufferUtils.putUnsignedShort(b, seqNum) - BufferUtils.putUnsignedShort(b, 0) - - b.array() - } - - override def equals(a: Any): Boolean = a match { - case o: MessageHeader => - messageType == o.messageType && - hopLimit == o.hopLimit && - origin == o.origin && - target == o.target && - hopCount == o.hopCount - // Don't compare length as it may be unknown (when header was just created without a body). - case _ => false - } + def length: Int = MessageHeader.Length } diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/RequestAddContact.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/RequestAddContact.scala index 2e31508..f318142 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/RequestAddContact.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/RequestAddContact.scala @@ -20,7 +20,9 @@ object RequestAddContact { */ case class RequestAddContact() extends MessageBody { - override def messageType = RequestAddContact.Type + override def protocolType = -1 + + override def contentType = RequestAddContact.Type override def write: Array[Byte] = { val b = ByteBuffer.allocate(length) diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ResultAddContact.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ResultAddContact.scala index 3cb2c09..7c61443 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ResultAddContact.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/ResultAddContact.scala @@ -25,7 +25,9 @@ object ResultAddContact { */ case class ResultAddContact(accepted: Boolean) extends MessageBody { - override def messageType = ResultAddContact.Type + override def protocolType = -1 + + override def contentType = ResultAddContact.Type override def write: Array[Byte] = { val b = ByteBuffer.allocate(length) 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 0b8fff4..af6bb86 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 @@ -28,7 +28,9 @@ object Text { */ case class Text(text: String, time: Date = new Date()) extends MessageBody { - override def messageType = Text.Type + override def protocolType = -1 + + override def contentType = Text.Type override def write: Array[Byte] = { val b = ByteBuffer.allocate(length) diff --git a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala index 4a6380b..29ddc87 100644 --- a/app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala +++ b/app/src/main/scala/com/nutomic/ensichat/protocol/messages/UserName.scala @@ -26,7 +26,9 @@ object UserName { */ case class UserName(name: String) extends MessageBody { - override def messageType = UserName.Type + override def protocolType = -1 + + override def contentType = UserName.Type override def write: Array[Byte] = { val b = ByteBuffer.allocate(length) diff --git a/app/src/main/scala/com/nutomic/ensichat/util/AddContactsHandler.scala b/app/src/main/scala/com/nutomic/ensichat/util/AddContactsHandler.scala index 9571ad8..269be32 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/AddContactsHandler.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/AddContactsHandler.scala @@ -7,7 +7,7 @@ import android.widget.Toast import com.nutomic.ensichat.R import com.nutomic.ensichat.activities.ConfirmAddContactActivity import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener -import com.nutomic.ensichat.protocol.messages.{RequestAddContact, Message, ResultAddContact} +import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact} import com.nutomic.ensichat.protocol.{Address, User} /** @@ -31,22 +31,22 @@ class AddContactsHandler(context: Context, getUser: (Address) => User, localAddr def onMessageReceived(msg: Message): Unit = { val remote = - if (msg.Header.origin == localAddress) - msg.Header.target + if (msg.header.origin == localAddress) + msg.header.target else - msg.Header.origin + msg.header.origin - msg.Body match { + msg.body match { case _: RequestAddContact => Log.i(Tag, "Remote device " + remote + " wants to add us as a contact") currentlyAdding += (remote -> new AddContactInfo(false, false)) // Don't show notification for requests coming from local device. - if (msg.Header.origin == localAddress) + if (msg.header.origin == localAddress) return val intent = new Intent(context, classOf[ConfirmAddContactActivity]) - intent.putExtra(ConfirmAddContactActivity.ExtraContactAddress, msg.Header.origin.toString) + intent.putExtra(ConfirmAddContactActivity.ExtraContactAddress, msg.header.origin.toString) val pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) @@ -66,10 +66,10 @@ class AddContactsHandler(context: Context, getUser: (Address) => User, localAddr } val newInfo = - if (msg.Header.origin == localAddress) - new AddContactInfo(res.accepted, currentlyAdding(remote).remoteConfirmed) - else - new AddContactInfo(currentlyAdding(remote).localConfirmed, res.accepted) + if (msg.header.origin == localAddress) + new AddContactInfo(res.accepted, currentlyAdding(remote).remoteConfirmed) + else + new AddContactInfo(currentlyAdding(remote).localConfirmed, res.accepted) currentlyAdding += (remote -> newInfo) if (res.accepted) 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 d3979bb..8e9da6d 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/Database.scala @@ -9,8 +9,8 @@ import com.nutomic.ensichat.protocol._ import com.nutomic.ensichat.protocol.messages._ import com.nutomic.ensichat.util.Database.OnContactsUpdatedListener -import scala.collection.{mutable, SortedSet} import scala.collection.immutable.TreeSet +import scala.collection.{SortedSet, mutable} object Database { @@ -22,8 +22,9 @@ object Database { "_id INTEGER PRIMARY KEY," + "origin TEXT NOT NULL," + "target TEXT NOT NULL," + + "message_id INT NOT NULL," + "text TEXT NOT NULL," + - "date INT NOT NULL);" // Unix timestamp of message. + "date INT NOT NULL);" // Unix timestamp private val CreateContactsTable = "CREATE TABLE contacts(" + "_id INTEGER PRIMARY KEY," + @@ -56,18 +57,14 @@ class Database(context: Context) */ def getMessages(address: Address, count: Int): SortedSet[Message] = { val c = getReadableDatabase.query(true, - "messages", Array("origin", "target", "text", "date"), + "messages", Array("origin", "target", "message_id", "text", "date"), "origin = ? OR target = ?", Array(address.toString, address.toString), null, null, "date DESC", count.toString) var messages = new TreeSet[Message]()(Message.Ordering) while (c.moveToNext()) { - val header = new MessageHeader( - Text.Type, - -1, - new Address(c.getString(c.getColumnIndex("origin"))), - new Address(c.getString(c.getColumnIndex("target"))), - -1, - -1) + val header = new ContentHeader(new Address(c.getString(c.getColumnIndex("origin"))), + new Address(c.getString(c.getColumnIndex("target"))), -1, Text.Type, + c.getLong(c.getColumnIndex("message_id"))) val body = new Text(new String(c.getString(c.getColumnIndex ("text"))), new Date(c.getLong(c.getColumnIndex("date")))) messages += new Message(header, body) @@ -79,12 +76,13 @@ class Database(context: Context) /** * Inserts the given new message into the database. */ - override def onMessageReceived(msg: Message): Unit = msg.Body match { + override def onMessageReceived(msg: Message): Unit = msg.body match { case text: Text => val cv = new ContentValues() - cv.put("origin", msg.Header.origin.toString) - cv.put("target", msg.Header.target.toString) - // toString used as workaround for compile error with Long. + cv.put("origin", msg.header.origin.toString) + cv.put("target", msg.header.target.toString) + // Need to use [[Long#toString]] because of https://issues.scala-lang.org/browse/SI-2991 + cv.put("message_id", msg.header.asInstanceOf[ContentHeader].messageId.toString) cv.put("date", text.time.getTime.toString) cv.put("text", text.text) getWritableDatabase.insert("messages", null, cv) diff --git a/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala b/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala index 067d22d..aa1a3d1 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala @@ -22,11 +22,11 @@ class MessagesAdapter(context: Context, remoteAddress: Address) extends val view = super.getView(position, convertView, parent).asInstanceOf[RelativeLayout] val tv = view.findViewById(android.R.id.text1).asInstanceOf[TextView] - tv.setText(getItem(position).Body.asInstanceOf[Text].text) + tv.setText(getItem(position).body.asInstanceOf[Text].text) val lp = new RelativeLayout.LayoutParams(tv.getLayoutParams) val margin = (MessageMargin * context.getResources.getDisplayMetrics.density).toInt - if (getItem(position).Header.origin != remoteAddress) { + if (getItem(position).header.origin != remoteAddress) { view.setGravity(Gravity.RIGHT) lp.setMargins(margin, 0, 0, 0) } else { diff --git a/app/src/main/scala/com/nutomic/ensichat/util/NotificationHandler.scala b/app/src/main/scala/com/nutomic/ensichat/util/NotificationHandler.scala index 7004b56..17b486d 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/NotificationHandler.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/NotificationHandler.scala @@ -1,12 +1,12 @@ package com.nutomic.ensichat.util -import android.app.{NotificationManager, Notification, PendingIntent} +import android.app.{Notification, NotificationManager, PendingIntent} import android.content.{Context, Intent} import com.nutomic.ensichat.R import com.nutomic.ensichat.activities.MainActivity import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener import com.nutomic.ensichat.protocol.Crypto -import com.nutomic.ensichat.protocol.messages.{Text, Message} +import com.nutomic.ensichat.protocol.messages.{Message, Text} /** * Displays notifications for new messages. @@ -15,9 +15,9 @@ class NotificationHandler(context: Context) extends OnMessageReceivedListener { private val notificationIdNewMessage = 1 - def onMessageReceived(msg: Message): Unit = msg.Body match { + def onMessageReceived(msg: Message): Unit = msg.body match { case text: Text => - if (msg.Header.origin == new Crypto(context).localAddress) + if (msg.header.origin == new Crypto(context).localAddress) return val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)