Added unique id to messages, allow extra header fields.
This commit is contained in:
parent
7bc7d01488
commit
5c102581ce
34 changed files with 428 additions and 208 deletions
62
PROTOCOL.md
62
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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(""))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 _ =>
|
||||
}
|
||||
|
|
|
@ -30,4 +30,4 @@ object BufferUtils {
|
|||
|
||||
def toString(array: Array[Byte]) = array.map("%02X".format(_)).mkString
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in a new issue