Added unique id to messages, allow extra header fields.

This commit is contained in:
Felix Ableitner 2015-04-16 00:17:10 +02:00
parent 7bc7d01488
commit 5c102581ce
34 changed files with 428 additions and 208 deletions

View File

@ -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

View File

@ -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)
}
}

View File

@ -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(""))
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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()

View File

@ -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

View File

@ -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}

View File

@ -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 {

View File

@ -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.

View File

@ -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 {

View File

@ -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 _ =>
}

View File

@ -30,4 +30,4 @@ object BufferUtils {
def toString(array: Array[Byte]) = array.map("%02X".format(_)).mkString
}
}

View File

@ -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)

View File

@ -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)
}
/**

View File

@ -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
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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)