diff --git a/PROTOCOL.md b/PROTOCOL.md index 5a6ff41..b7f9dc4 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -7,17 +7,17 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. -A node is a single device implementing this protocol. Each node has -exactl one node address. +A _node_ is a single device implementing this protocol. Each node has +exactly one node address based on its RSA key pair. -A node address consists of 32 bytes and is the SHA-256 hash of the +A _node address_ consists of 32 bytes and is the SHA-256 hash of the node's public key. -The broadcast address is +The _broadcast address_ is `0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF` (i.e. all bits set). -The null address is +The _null address_ is `0x0000000000000000000000000000000000000000000000000000000000000000` (i.e. no bits set). @@ -29,6 +29,27 @@ nodes MUST NOT connect to a node with 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. + + 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 (variable length) \ + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + \ Encryption Data (variable length) \ + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + \ Body (variable length) \ + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + ### Header Every message starts with one 32 bit word indicating the message @@ -54,6 +75,8 @@ header is in network byte order, i.e. big endian. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | Metric | Reserved | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Body Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Ver specifies the protocol version number. This is currently 0. A message with unknown version number MUST be ignored. The connection @@ -83,17 +106,50 @@ Sequence number is the sequence number of either the source or target node for this message, depending on type. +### Encryption 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 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Signature Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + \ Signature (variable length) \ + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Encryption Key Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + \ Encryption Key (variable length) \ + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +Encryption key is the symmetric key that was used to encrypt the message +body. + +Signature is the cryptographic signature over the (unencrypted) message +header and message body. + + + ConnectionInfo (Type = 0) --------- After successfully connecting to a node via Bluetooth, public keys -must be exchanged. Each node MUST send this as the first message over +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. A receiving node SHOULD store the key in permanent storage if it -hasn't already stored it earlier. This key is to be used for message +hasn't already stored it earlier. However, a node MAY decide to +delete these stored keys in a least-recently-used order to adhere +to storage limitations. If a key has been deleted, messages to +that node can only be sent once a new ConnectionInfo message +for it has been received. + + +This key is to be used for message encryption when communicating with the sending node. 0 1 2 3 @@ -114,20 +170,46 @@ After this message has been received, communication with normal messages may start. -### Data (Data Transfer, Type = 255) +### RequestAddContact (Type = 4) + +Sent when a user wants to add another node as a contact. After this, +a ResultAddContact message should be returned. 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Length | + | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +### ResultAddContact (Type = 5) + +Sent as response to a RequestAddContact message. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |A| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +Accepted bit (A) is true if the user accepts the new contact, false +otherwise. Nodes should only add another node as a contact if both +users agreed. + +### Text (Type = 6) + +A simple chat message. + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Text Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ / / - \ Data (variable length) \ + \ Text (variable length) \ / / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -Length is the number of bytes in data. - -Data is any binary data that should be transported. - -This message type is deprecated. \ No newline at end of file +Text the string to be transferred, encoded as UTF-8. diff --git a/app/build.gradle b/app/build.gradle index 451f567..60c490c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,27 +6,19 @@ buildscript { mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:0.14.4" classpath "jp.leafytree.gradle:gradle-android-scala-plugin:1.3.1" } } dependencies { - compile "com.android.support:support-v4:21.0.0" + compile "com.android.support:support-v4:21.0.2" // For `flat` debug config, as `flatProvided` is unknown. provided "org.scala-lang:scala-library:2.11.4" debugCompile "org.scala-lang:scala-library:2.11.4" releaseCompile "org.scala-lang:scala-library:2.11.4" - compile("org.msgpack:msgpack-scala_2.11:0.6.11") { - transitive = false; - } - compile('org.msgpack:msgpack:0.6.11') { - transitive = false; - } compile 'com.google.guava:guava:18.0' } - android { compileSdkVersion 21 buildToolsVersion "21.1.1" @@ -40,13 +32,8 @@ android { } sourceSets { - main { - scala.srcDir "src/main/scala" - } - - androidTest { - scala.srcDir "src/androidTest/scala" - } + main.scala.srcDir "src/main/scala" + androidTest.scala.srcDir "src/androidTest/scala" } buildTypes { @@ -68,11 +55,13 @@ android { // Needed to rename `app-thin.apk` to `app-debug.apk` (because Android Studio doesn't let us // specify a different apk name). +/* applicationVariants.all { variant -> def apk = variant.outputFile; def newName = apk.name.replace("app-thin", "app-debug"); variant.outputFile = new File(apk.parentFile, newName); } +*/ // Avoid duplicate file errors during packaging. packagingOptions { @@ -82,3 +71,7 @@ android { exclude 'META-INF/NOTICE' } } + +tasks.withType(ScalaCompile) { + scalaCompileOptions.useCompileDaemon = true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 3d4b47b..218d085 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,11 +18,12 @@ -dontpreverify -dontwarn scala.** -keep class !scala*.** { *; } --ignorewarnings # Avoid crash when invoking String.toInt (see https://issues.scala-lang.org/browse/SI-5397). -keep class scala.collection.SeqLike { public protected *; } -# Suppress warnings caused by msgpack (code works fine anyway). --dontwarn +# Disable warnings for Guava annotations. +-dontwarn javax.annotation.** +-dontwarn javax.inject.** +-dontwarn sun.misc.Unsafe diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/AddressTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/AddressTest.scala index f464598..b350a40 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/AddressTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/AddressTest.scala @@ -14,14 +14,17 @@ object AddressTest { val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36") - val a1Binary: Array[Byte] = Array(-91, 27, 116, 71, 94, -26, 34, -61, -55, 36, -37, 20, 118, 104, -8, 94, 2, 76, -96, -76, 76, -95, 70, -75, -29, -45, -61, 26, 84, -77, 76, 30).map(_.toByte) + val a1Binary: Array[Byte] = Array(-91, 27, 116, 71, 94, -26, 34, -61, -55, 36, -37, 20, 118, 104, + -8, 94, 2, 76, -96, -76, 76, -95, 70, -75, -29, -45, -61, 26, 84, -77, 76, 30).map(_.toByte) + + val Addresses = Set(a1, a2, a3, a4, Address.Broadcast, Address.Null) } class AddressTest extends AndroidTestCase { def testEncode(): Unit = { - Set(Address.Broadcast, Address.Null, a1, a2, a3, a4).foreach{a => + Addresses.foreach{a => val base32 = a.toString val read = new Address(base32) assertEquals(a, read) diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageHeaderTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageHeaderTest.scala index 49310d5..bfdf560 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageHeaderTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageHeaderTest.scala @@ -1,30 +1,41 @@ package com.nutomic.ensichat.aodvv2 -import java.util.Date +import java.util.GregorianCalendar import android.test.AndroidTestCase -import junit.framework.Assert +import com.nutomic.ensichat.aodvv2.MessageHeaderTest._ +import junit.framework.Assert._ object MessageHeaderTest { - val h1 = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit, new Date(), AddressTest.a3, - AddressTest.a4, 456, 123) + val h1 = new MessageHeader(Text.Type, MessageHeader.DefaultHopLimit, AddressTest.a1, + AddressTest.a2, 1234, 0, new GregorianCalendar(1970, 1, 1).getTime, 567, 8) - val h2 = new MessageHeader(0xfff, 0, new Date(0xffffffff), Address.Null, Address.Broadcast, 0, - 0xff) + val h2 = new MessageHeader(Text.Type, 0, AddressTest.a1, AddressTest.a3, 8765, 234, + new GregorianCalendar(2014, 6, 10).getTime, 0, 0xff) - val h3 = new MessageHeader(0xfff, 0xff, new Date(0), Address.Broadcast, Address.Null, 0xffff, 0) + val h3 = new MessageHeader(Text.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56, + new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0) + + val h4 = new MessageHeader(0xfff, 0, Address.Null, Address.Broadcast, 0, 0xff, + new GregorianCalendar(1990, 1, 1).getTime, 0, 0xff) + + val h5 = new MessageHeader(ConnectionInfo.Type, 0xff, Address.Broadcast, Address.Null, 0xffff, 0, + new GregorianCalendar(2035, 12, 31).getTime, 0xffff, 0) + + val headers = Set(h1, h2, h3, h4, h5) } class MessageHeaderTest extends AndroidTestCase { def testSerialize(): Unit = { - val ci = ConnectionInfoTest.generateCi(getContext) - val bytes = MessageHeaderTest.h1.write(ci) - val header = MessageHeader.read(bytes) - Assert.assertEquals(MessageHeaderTest.h1, header) - Assert.assertEquals(bytes.length, header.Length) + headers.foreach{h => + val bytes = h.write(0) + val header = MessageHeader.read(bytes) + assertEquals(h, header) + assertEquals(bytes.length, header.Length) + } } } \ No newline at end of file diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageTest.scala new file mode 100644 index 0000000..fa8f8b3 --- /dev/null +++ b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageTest.scala @@ -0,0 +1,77 @@ +package com.nutomic.ensichat.aodvv2 + +import java.io.ByteArrayInputStream +import java.util.GregorianCalendar + +import android.test.AndroidTestCase +import com.nutomic.ensichat.aodvv2.MessageHeaderTest._ +import com.nutomic.ensichat.aodvv2.MessageTest._ +import com.nutomic.ensichat.messages.Crypto +import junit.framework.Assert._ + +import scala.collection.immutable.TreeSet + +object MessageTest { + + val m1 = new Message(h1, new Text("first")) + + val m2 = new Message(h2, new Text("second")) + + val m3 = new Message(h3, new Text("third")) + + val messages = Set(m1, m2, m3) + +} + +class MessageTest extends AndroidTestCase { + + lazy val Crypto: Crypto = new Crypto(getContext) + + override def setUp(): Unit = { + super.setUp() + if (!Crypto.localKeysExist) { + Crypto.generateLocalKeys() + } + } + + def testOrder(): Unit = { + var messages = new TreeSet[Message]()(Message.Ordering) + messages += m1 + messages += m2 + assertEquals(m1, messages.firstKey) + + messages = new TreeSet[Message]()(Message.Ordering) + messages += m2 + messages += m3 + assertEquals(m2, messages.firstKey) + } + + def testSerializeSigned(): Unit = { + val header = new MessageHeader(ConnectionInfo.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56, + new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0) + val m = new Message(header, ConnectionInfoTest.generateCi(getContext)) + + val signed = Crypto.sign(m) + val bytes = signed.write + val read = Message.read(new ByteArrayInputStream(bytes)) + + assertEquals(signed, read) + assertTrue(Crypto.verify(read, Crypto.getLocalPublicKey)) + } + + def testSerializeEncrypted(): Unit = { + messages.foreach{ m => + val signed = Crypto.sign(m) + val encrypted = Crypto.encrypt(signed, Crypto.getLocalPublicKey) + val bytes = encrypted.write + + val read = Message.read(new ByteArrayInputStream(bytes)) + assertEquals(encrypted.Crypto, read.Crypto) + val decrypted = Crypto.decrypt(read) + assertEquals(m.Header, decrypted.Header) + assertEquals(m.Body, decrypted.Body) + assertTrue(Crypto.verify(decrypted, Crypto.getLocalPublicKey)) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/ResultAddContactTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/ResultAddContactTest.scala new file mode 100644 index 0000000..92ac748 --- /dev/null +++ b/app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/ResultAddContactTest.scala @@ -0,0 +1,18 @@ +package com.nutomic.ensichat.aodvv2 + +import android.test.AndroidTestCase +import junit.framework.Assert._ + + +class ResultAddContactTest extends AndroidTestCase { + + def testWriteRead(): Unit = { + Array(true, false).foreach { a => + val rac = new ResultAddContact(a) + val bytes = rac.write + val read = ResultAddContact.read(bytes) + assertEquals(a, read.Accepted) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/messages/CryptoTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/messages/CryptoTest.scala index f97dc51..0b67fd6 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/messages/CryptoTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/messages/CryptoTest.scala @@ -1,7 +1,7 @@ package com.nutomic.ensichat.messages import android.test.AndroidTestCase -import com.nutomic.ensichat.messages.MessageTest._ +import com.nutomic.ensichat.aodvv2.MessageTest._ import junit.framework.Assert._ class CryptoTest extends AndroidTestCase { @@ -16,15 +16,21 @@ class CryptoTest extends AndroidTestCase { } def testSignVerify(): Unit = { - val sig = Crypto.calculateSignature(m1) - assertTrue(Crypto.isValidSignature(m1, sig, Crypto.getLocalPublicKey)) + messages.foreach { m => + val signed = Crypto.sign(m) + assertTrue(Crypto.verify(signed, Crypto.getLocalPublicKey)) + assertEquals(m.Header, signed.Header) + assertEquals(m.Body, signed.Body) + } } def testEncryptDecrypt(): Unit = { - val (encrypted, key) = - Crypto.encrypt(null, MessageTest.m1.write(Array[Byte]()), Crypto.getLocalPublicKey) - val decrypted = Crypto.decrypt(encrypted, key) - assertEquals(MessageTest.m1, Message.read(decrypted)._1) + 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) + } } } diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/messages/MessageTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/messages/MessageTest.scala deleted file mode 100644 index bc09824..0000000 --- a/app/src/androidTest/scala/com/nutomic/ensichat/messages/MessageTest.scala +++ /dev/null @@ -1,50 +0,0 @@ -package com.nutomic.ensichat.messages - -import java.io.{PipedInputStream, PipedOutputStream} -import java.util.GregorianCalendar - -import android.test.AndroidTestCase -import com.nutomic.ensichat.aodvv2.AddressTest -import com.nutomic.ensichat.messages.MessageTest._ -import junit.framework.Assert._ - -import scala.collection.immutable.TreeSet - -object MessageTest { - - val m1 = new TextMessage(AddressTest.a1, AddressTest.a2, - new GregorianCalendar(2014, 10, 29).getTime, "first") - - val m2 = new TextMessage(AddressTest.a1, AddressTest.a3, - new GregorianCalendar(2014, 10, 30).getTime, "second") - - val m3 = new TextMessage(AddressTest.a4, AddressTest.a2, - new GregorianCalendar(2014, 10, 31).getTime, "third") - -} - -class MessageTest extends AndroidTestCase { - - def testSerialize(): Unit = { - Set(m1, m2, m3).foreach { m => - val pis = new PipedInputStream() - val pos = new PipedOutputStream(pis) - val bytes = m.write(Array[Byte]()) - val (msg, _) = Message.read(bytes) - assertEquals(m, msg) - } - } - - def testOrder(): Unit = { - var messages = new TreeSet[Message]()(Message.Ordering) - messages += MessageTest.m1 - messages += MessageTest.m2 - assertEquals(MessageTest.m1, messages.firstKey) - - messages = new TreeSet[Message]()(Message.Ordering) - messages += MessageTest.m2 - messages += MessageTest.m3 - assertEquals(MessageTest.m2, messages.firstKey) - } - -} diff --git a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala b/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala index ecec1c9..853eded 100644 --- a/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala +++ b/app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala @@ -9,7 +9,7 @@ import android.database.sqlite.SQLiteDatabase import android.test.AndroidTestCase import android.test.mock.MockContext import com.nutomic.ensichat.aodvv2.AddressTest -import com.nutomic.ensichat.messages.MessageTest +import com.nutomic.ensichat.aodvv2.MessageTest._ import junit.framework.Assert._ class DatabaseTest extends AndroidTestCase { @@ -27,9 +27,9 @@ class DatabaseTest extends AndroidTestCase { private lazy val Database = new Database(new TestContext(getContext)) override def setUp(): Unit = { - Database.addMessage(MessageTest.m1) - Database.addMessage(MessageTest.m2) - Database.addMessage(MessageTest.m3) + Database.addMessage(m1) + Database.addMessage(m2) + Database.addMessage(m3) } override def tearDown(): Unit = { @@ -38,22 +38,22 @@ class DatabaseTest extends AndroidTestCase { } def testMessageCount(): Unit = { - val msg1 = Database.getMessages(MessageTest.m1.sender, 1) + val msg1 = Database.getMessages(m1.Header.Origin, 1) assertEquals(1, msg1.size) - val msg2 = Database.getMessages(MessageTest.m1.sender, 3) + val msg2 = Database.getMessages(m1.Header.Origin, 3) assertEquals(2, msg2.size) } def testMessageOrder(): Unit = { - val msg = Database.getMessages(MessageTest.m1.receiver, 1) - assertTrue(msg.contains(MessageTest.m3)) + val msg = Database.getMessages(m1.Header.Target, 1) + assertTrue(msg.contains(m3)) } def testMessageSelect(): Unit = { - val msg = Database.getMessages(MessageTest.m1.receiver, 2) - assertTrue(msg.contains(MessageTest.m1)) - assertTrue(msg.contains(MessageTest.m3)) + val msg = Database.getMessages(m1.Header.Target, 2) + assertTrue(msg.contains(m1)) + assertTrue(msg.contains(m3)) } def testAddContact(): Unit = { diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 363cb03..70712bb 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala index 8934500..ebe57d7 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala @@ -1,7 +1,5 @@ package com.nutomic.ensichat.activities -import java.util.Date - import android.app.AlertDialog import android.content.DialogInterface.OnClickListener import android.content.{Context, DialogInterface} @@ -12,10 +10,10 @@ import android.view._ import android.widget.AdapterView.OnItemClickListener import android.widget._ import com.nutomic.ensichat.R -import com.nutomic.ensichat.aodvv2.Address +import com.nutomic.ensichat.aodvv2.{Address, Message, RequestAddContact, ResultAddContact} import com.nutomic.ensichat.bluetooth.ChatService import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener -import com.nutomic.ensichat.messages.{Crypto, Message, RequestAddContactMessage, ResultAddContactMessage} +import com.nutomic.ensichat.messages.Crypto import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator} import scala.collection.SortedSet @@ -92,7 +90,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont return } - service.send(new RequestAddContactMessage(Crypto.getLocalAddress, address, new Date())) + service.sendTo(address, new RequestAddContact()) addDeviceDialog(address) } @@ -108,12 +106,10 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont currentlyAdding += (address -> new AddContactInfo(currentlyAdding(address).localConfirmed, true)) addContactIfBothConfirmed(address) - service.send( - new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), true)) + service.sendTo(address, new ResultAddContact(true)) case DialogInterface.BUTTON_NEGATIVE => // Local user denied adding contact, send info to other device. - service.send( - new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), false)) + service.sendTo(address, new ResultAddContact(false)) } } @@ -137,27 +133,28 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont } /** - * Handles incoming [[RequestAddContactMessage]] and [[ResultAddContactMessage]] messages. + * Handles incoming [[RequestAddContact]] and [[ResultAddContact]] messages. * * These are only handled here and require user action, so contacts can only be added if * the user is in this activity. */ override def onMessageReceived(messages: SortedSet[Message]): Unit = { - messages.filter(_.receiver == Crypto.getLocalAddress) + messages.filter(_.Header.Target == Crypto.getLocalAddress) .foreach{ - case m: RequestAddContactMessage => - Log.i(Tag, "Remote device " + m.sender + " wants to add us as a contact, showing dialog") - addDeviceDialog(m.sender) - case m: ResultAddContactMessage => - if (m.Accepted) { - Log.i(Tag, "Remote device " + m.sender + " accepted us as a contact, updating state") - currentlyAdding += (m.sender -> - new AddContactInfo(true, currentlyAdding(m.sender).remoteConfirmed)) - addContactIfBothConfirmed(m.sender) + case m if m.Body.isInstanceOf[RequestAddContact] => + Log.i(Tag, "Remote device " + m.Header.Origin + " wants to add us as a contact, showing dialog") + addDeviceDialog(m.Header.Origin) + case m if m.Body.isInstanceOf[ResultAddContact] => + val origin = m.Header.Origin + if (m.Body.asInstanceOf[ResultAddContact].Accepted) { + Log.i(Tag, "Remote device " + origin + " accepted us as a contact, updating state") + currentlyAdding += (origin -> + new AddContactInfo(true, currentlyAdding(origin).remoteConfirmed)) + addContactIfBothConfirmed(origin) } else { - Log.i(Tag, "Remote device " + m.sender + " denied us as a contact, showing toast") + Log.i(Tag, "Remote device " + origin + " denied us as a contact, showing toast") Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show() - currentlyAdding -= m.sender + currentlyAdding -= origin } case _ => } diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/ConnectionInfo.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/ConnectionInfo.scala index 83d6669..ecd55e9 100644 --- a/app/src/main/scala/com/nutomic/ensichat/aodvv2/ConnectionInfo.scala +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/ConnectionInfo.scala @@ -34,11 +34,22 @@ object ConnectionInfo { */ class ConnectionInfo(val key: PublicKey) extends MessageBody { + override def Type = ConnectionInfo.Type + override def write: Array[Byte] = { - val b = ByteBuffer.allocate(4 + key.getEncoded.length) + val b = ByteBuffer.allocate(length) BufferUtils.putUnsignedInt(b, key.getEncoded.length) b.put(key.getEncoded) b.array() } + override def equals(a: Any): Boolean = a match { + case o: ConnectionInfo => key == o.key + case _ => false + } + + override def toString = "ConnectionInfo(key=" + key + ")" + + override def length = 4 + key.getEncoded.length + } diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/CryptoData.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/CryptoData.scala new file mode 100644 index 0000000..3172e39 --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/CryptoData.scala @@ -0,0 +1,67 @@ +package com.nutomic.ensichat.aodvv2 + +import java.nio.ByteBuffer +import java.util.Arrays + +import com.nutomic.ensichat.util.BufferUtils + +object CryptoData { + + /** + * Constructs [[CryptoData]] instance from byte array. + */ + def read(array: Array[Byte]): (CryptoData, Array[Byte]) = { + val b = ByteBuffer.wrap(array) + val signatureLength = BufferUtils.getUnsignedInt(b).toInt + val signature = new Array[Byte](signatureLength) + b.get(signature, 0, signatureLength) + + val keyLength = BufferUtils.getUnsignedInt(b).toInt + val key = + if (keyLength != 0) { + val key = new Array[Byte](keyLength) + b.get(key, 0, keyLength) + Some(key) + } + else None + + val remaining = new Array[Byte](b.remaining()) + b.get(remaining, 0, b.remaining()) + (new CryptoData(Some(signature), key), remaining) + + } + +} + +/** + * Holds the signature and (optional) key that are stored in a message. + */ +class CryptoData(val Signature: Option[Array[Byte]], val Key: Option[Array[Byte]]) { + + override def equals(a: Any): Boolean = a match { + case o: CryptoData => + Arrays.equals(Signature.orNull, o.Signature.orNull) && Arrays.equals(Key.orNull, o.Key.orNull) + case _ => false + } + + /** + * Writes this object into a new byte array. + * @return + */ + def write: Array[Byte] = { + val b = ByteBuffer.allocate(length) + BufferUtils.putUnsignedInt(b, Signature.get.length) + b.put(Signature.get) + BufferUtils.putUnsignedInt(b, keyLength) + if (Key.nonEmpty) b.put(Key.get) + b.array() + } + + def length = 8 + Signature.get.length + keyLength + + private def keyLength = if (Key.isDefined) Key.get.length else 0 + + override def toString = "CryptoData(Signature.length=" + Signature.foreach(_.length) + + ", Key.length=" + Key.foreach(_.length) + ")" + +} \ No newline at end of file diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/Data.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/Data.scala deleted file mode 100644 index eadaac9..0000000 --- a/app/src/main/scala/com/nutomic/ensichat/aodvv2/Data.scala +++ /dev/null @@ -1,37 +0,0 @@ -package com.nutomic.ensichat.aodvv2 - -import java.nio.ByteBuffer - -import com.nutomic.ensichat.util.BufferUtils - -object Data { - - val Type = 255 - - /** - * Constructs [[Data]] object from byte array. - */ - def read(array: Array[Byte]): Data = { - val b = ByteBuffer.wrap(array) - val length = BufferUtils.getUnsignedInt(b).toInt - val data = new Array[Byte](length) - b.get(data, 0, length) - new Data(data) - } - -} - -/** - * Container for [[com.nutomic.ensichat.messages.Message]] objects. - */ -@Deprecated -class Data(val data: Array[Byte]) extends MessageBody { - - override def write: Array[Byte] = { - val b = ByteBuffer.allocate(4 + data.length) - BufferUtils.putUnsignedInt(b, data.length) - b.put(data) - b.array() - } - -} diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/EncryptedBody.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/EncryptedBody.scala new file mode 100644 index 0000000..956f558 --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/EncryptedBody.scala @@ -0,0 +1,15 @@ +package com.nutomic.ensichat.aodvv2 + +/** + * Represents the data in an encrypted message body. + */ +class EncryptedBody(val Data: Array[Byte]) extends MessageBody { + + override def Type = -1 + + def write = Data + + override def toString = "EncryptedBody(Data.length=" + Data.length + ")" + + override def length = Data.length +} diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala new file mode 100644 index 0000000..d40e66d --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala @@ -0,0 +1,50 @@ +package com.nutomic.ensichat.aodvv2 + +import java.io.InputStream + +object Message { + + /** + * Orders messages by date, oldest messages first. + */ + val Ordering = new Ordering[Message] { + override def compare(m1: Message, m2: Message) = m1.Header.Time.compareTo(m2.Header.Time) + } + + def read(stream: InputStream): Message = { + val headerBytes = new Array[Byte](MessageHeader.Length) + stream.read(headerBytes, 0, MessageHeader.Length) + val header = MessageHeader.read(headerBytes) + + val contentLength = (header.Length - MessageHeader.Length).toInt + val contentBytes = new Array[Byte](contentLength) + stream.read(contentBytes, 0, contentLength) + + val (crypto, remaining) = CryptoData.read(contentBytes) + + val body = + header.MessageType match { + case ConnectionInfo.Type => ConnectionInfo.read(remaining) + case _ => new EncryptedBody(remaining) + } + + new Message(header, crypto, body) + } + +} + +class Message(val Header: MessageHeader, val Crypto: CryptoData, val Body: MessageBody) { + + def this(header: MessageHeader, body: MessageBody) = + this(header, new CryptoData(None, None), body) + + def write = Header.write(Body.length + Crypto.length) ++ Crypto.write ++ Body.write + + override def toString = "Message(Header=" + Header + ", Body=" + Body + ", Crypto=" + Crypto + ")" + + override def equals(a: Any): Boolean = a match { + case o: Message => Header == o.Header && Body == o.Body && Crypto == o.Crypto + case _ => false + } + +} \ No newline at end of file diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageBody.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageBody.scala index 8850994..2564489 100644 --- a/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageBody.scala +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageBody.scala @@ -1,14 +1,19 @@ package com.nutomic.ensichat.aodvv2 +import android.util.Log + /** * Holds the actual message content. */ abstract class MessageBody { + def Type: Int + /** * Writes the message contents to a byte array. - * @return */ def write: Array[Byte] + def length: Int + } diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageHeader.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageHeader.scala index a69e0a1..293b8d8 100644 --- a/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageHeader.scala +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageHeader.scala @@ -7,11 +7,11 @@ import com.nutomic.ensichat.util.BufferUtils object MessageHeader { - val Length = 16 + 2 * Address.Length + val Length = 20 + 2 * Address.Length val DefaultHopLimit = 20 - val Version = 3 + val Version = 0 class ParseMessageException(detailMessage: String) extends RuntimeException(detailMessage) { } @@ -38,7 +38,7 @@ object MessageHeader { val seqNum = BufferUtils.getUnsignedShort(b) val metric = BufferUtils.getUnsignedByte(b) - new MessageHeader(messageType, hopLimit, time, origin, target, seqNum, metric, length, hopCount) + new MessageHeader(messageType, hopLimit, origin, target, seqNum, metric, time, length, hopCount) } } @@ -48,27 +48,26 @@ object MessageHeader { */ class MessageHeader(val MessageType: Int, val HopLimit: Int, - val Time: Date, val Origin: Address, val Target: Address, val SequenceNumber: Int, val Metric: Int, + val Time: Date = new Date(), val Length: Long = -1, val HopCount: Int = 0) { /** * Writes the header to byte array. */ - def write(body: MessageBody): Array[Byte] = { + def write(contentLength: Int): Array[Byte] = { val b = ByteBuffer.allocate(MessageHeader.Length) - val bodyBytes = body.write val versionAndType = (MessageHeader.Version << 12) | MessageType BufferUtils.putUnsignedShort(b, versionAndType) BufferUtils.putUnsignedByte(b, HopLimit) BufferUtils.putUnsignedByte(b, HopCount) - BufferUtils.putUnsignedInt(b, MessageHeader.Length + bodyBytes.length) + BufferUtils.putUnsignedInt(b, MessageHeader.Length + contentLength) b.putInt((Time.getTime / 1000).toInt) b.put(Origin.Bytes) b.put(Target.Bytes) @@ -77,7 +76,7 @@ class MessageHeader(val MessageType: Int, BufferUtils.putUnsignedByte(b, Metric) BufferUtils.putUnsignedByte(b, 0) - b.array() ++ bodyBytes + b.array() } override def equals(a: Any): Boolean = a match { @@ -89,9 +88,8 @@ class MessageHeader(val MessageType: Int, Target == o.Target && SequenceNumber == o.SequenceNumber && Metric == o.Metric && - // Don't compare length as it may be unknown (when header was just created without a body). - //Length == o.Length && HopCount == o.HopCount + // Don't compare length as it may be unknown (when header was just created without a body). case _ => false } diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/RequestAddContact.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/RequestAddContact.scala new file mode 100644 index 0000000..1434597 --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/RequestAddContact.scala @@ -0,0 +1,34 @@ +package com.nutomic.ensichat.aodvv2 + +import java.nio.ByteBuffer + +object RequestAddContact { + + val Type = 4 + + /** + * Constructs [[RequestAddContact]] instance from byte array. + */ + def read(array: Array[Byte]): RequestAddContact = { + new RequestAddContact() + } + +} + +/** + * Sent when the user initiates adding another device as a contact. + */ +class RequestAddContact extends MessageBody { + + override def Type = RequestAddContact.Type + + override def write: Array[Byte] = { + val b = ByteBuffer.allocate(length) + b.array() + } + + override def toString = "RequestAddContact()" + + override def length = 4 + +} diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/ResultAddContact.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/ResultAddContact.scala new file mode 100644 index 0000000..191f868 --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/ResultAddContact.scala @@ -0,0 +1,41 @@ +package com.nutomic.ensichat.aodvv2 + +import java.nio.ByteBuffer + +import com.nutomic.ensichat.util.BufferUtils + +object ResultAddContact { + + val Type = 5 + + /** + * Constructs [[ResultAddContact]] instance from byte array. + */ + def read(array: Array[Byte]): ResultAddContact = { + val b = ByteBuffer.wrap(array) + val first = BufferUtils.getUnsignedByte(b) + val accepted = (first & 0x80) != 0 + new ResultAddContact(accepted) + } + +} + +/** + * Contains the result of a [[RequestAddContact]] message. + */ +class ResultAddContact(val Accepted: Boolean) extends MessageBody { + + override def Type = ResultAddContact.Type + + override def write: Array[Byte] = { + val b = ByteBuffer.allocate(length) + BufferUtils.putUnsignedByte(b, if (Accepted) 0x80 else 0) + (0 to 1).foreach(_ => BufferUtils.putUnsignedByte(b, 0)) + b.array() + } + + override def toString = "ResultAddContact(Accepted=" + Accepted + ")" + + override def length = 4 + +} diff --git a/app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala b/app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala new file mode 100644 index 0000000..0b7e04d --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala @@ -0,0 +1,50 @@ +package com.nutomic.ensichat.aodvv2 + +import java.nio.ByteBuffer + +import com.nutomic.ensichat.util.BufferUtils + +object Text { + + val Type = 6 + + val Charset = "UTF-8" + + /** + * Constructs [[Text]] instance from byte array. + */ + def read(array: Array[Byte]): Text = { + val b = ByteBuffer.wrap(array) + val length = BufferUtils.getUnsignedInt(b).toInt + val bytes = new Array[Byte](length) + b.get(bytes, 0, length) + new Text(new String(bytes, Text.Charset)) + } + +} + +/** + * Holds a plain text message. + */ +class Text(val text: String) extends MessageBody { + + override def Type = Text.Type + + override def write: Array[Byte] = { + val bytes = text.getBytes(Text.Charset) + val b = ByteBuffer.allocate(4 + bytes.length) + BufferUtils.putUnsignedInt(b, bytes.length) + b.put(bytes) + b.array() + } + + override def equals(a: Any): Boolean = a match { + case o: Text => text == o.text + case _ => false + } + + override def toString = "Text(" + text + ")" + + override def length = write.length + +} diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala index 5352412..daee8b7 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala @@ -1,6 +1,6 @@ package com.nutomic.ensichat.bluetooth -import java.util.{Date, UUID} +import java.util.UUID import android.app.Service import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket} @@ -14,7 +14,6 @@ import com.nutomic.ensichat.aodvv2._ import com.nutomic.ensichat.bluetooth.ChatService.{OnMessageReceivedListener, OnNearbyContactsChangedListener} import com.nutomic.ensichat.messages._ import com.nutomic.ensichat.util.Database -import org.msgpack.ScalaMessagePack import scala.collection.SortedSet import scala.collection.immutable.{HashMap, HashSet, TreeSet} @@ -77,6 +76,8 @@ class ChatService extends Service { private lazy val Crypto = new Crypto(this) + private var discovered = Set[Device]() + private val AddressDeviceMap = HashBiMap.create[Address, Device.ID]() /** @@ -89,6 +90,8 @@ class ChatService extends Service { registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND)) registerReceiver(BluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) + registerReceiver(DiscoveryFinishedReceiver, + new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) if (bluetoothAdapter.isEnabled) { startBluetoothConnections() } @@ -116,6 +119,7 @@ class ChatService extends Service { cancelDiscovery = true unregisterReceiver(DeviceDiscoveredReceiver) unregisterReceiver(BluetoothStateReceiver) + unregisterReceiver(DiscoveryFinishedReceiver) } /** @@ -126,12 +130,12 @@ class ChatService extends Service { return if (!bluetoothAdapter.isDiscovering) { - Log.v(Tag, "Running discovery") + Log.v(Tag, "Starting discovery") bluetoothAdapter.startDiscovery() } val scanInterval = PreferenceManager.getDefaultSharedPreferences(this) - .getString("scan_interval_seconds", "5").toInt * 1000 + .getString("scan_interval_seconds", "15").toInt * 1000 MainHandler.postDelayed(new Runnable { override def run(): Unit = discover() }, scanInterval) @@ -142,10 +146,21 @@ class ChatService extends Service { */ private val DeviceDiscoveredReceiver = new BroadcastReceiver() { override def onReceive(context: Context, intent: Intent) { - val device: Device = - new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false) - devices += (device.Id -> device) - new ConnectThread(device, onConnectionChanged).start() + discovered += new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false) + } + } + + /** + * Iniates the actual connection to discovered devices. + */ + private val DiscoveryFinishedReceiver = new BroadcastReceiver() { + override def onReceive(context: Context, intent: Intent): Unit = { + discovered.filterNot(d => connections.keySet.contains(d.Id)) + .foreach { d => + new ConnectThread(d, onConnectionChanged).start() + devices += (d.Id -> d) + } + discovered = Set[Device]() } } @@ -200,7 +215,7 @@ class ChatService extends Service { def onConnectionChanged(device: Device, socket: BluetoothSocket): Unit = { devices += (device.Id -> device) - if (device.Connected) { + if (device.Connected && !connections.keySet.contains(device.Id)) { connections += (device.Id -> new TransferThread(device, socket, this, Crypto, onReceiveMessage)) connections(device.Id).start() @@ -219,79 +234,75 @@ class ChatService extends Service { } /** - * Sends message to the device specified as receiver, + * Sends a new message to the given target address. */ - def send(message: Message): Unit = { - assert(message.sender == Crypto.getLocalAddress, "Message must be sent from local device") - - if (!AddressDeviceMap.containsKey(message.receiver)) { - Log.w(Tag, "Receiver " + message.receiver + " is not connected, ignoring message") + def sendTo(target: Address, body: MessageBody): Unit = { + if (!AddressDeviceMap.containsKey(target)) { + Log.w(Tag, "Receiver " + target + " is not connected, ignoring message") return } - val header = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit, - new Date(), Crypto.getLocalAddress, message.receiver, 0, 0) + val header = new MessageHeader(body.Type, MessageHeader.DefaultHopLimit, + Crypto.getLocalAddress, target, 0, 0) - val plain = message.write(Crypto.calculateSignature(message)) - val (encrypted, key) = Crypto.encrypt(message.receiver, plain) - val packer = new ScalaMessagePack().createBufferPacker() - packer - .write(encrypted) - .write(key) - val body = new Data(packer.toByteArray) - - connections.apply(AddressDeviceMap.get(message.receiver)).send(header, body) - Database.addMessage(message) + val msg = new Message(header, body) + val encrypted = Crypto.encrypt(Crypto.sign(msg)) + connections.apply(AddressDeviceMap.get(target)).send(encrypted) + Database.addMessage(msg) + callMessageReceivedListeners(msg) } /** * Saves the message to database and sends it to registered listeners. * - * If you want to send a new message, use [[send]]. + * If you want to send a new message, use [[sendTo]]. * * Messages must always be sent between local device and a contact. * - * NOTE: Messages sent from the local node using [[send]] are also passed through this method. + * NOTE: Messages sent from the local node using [[sendTo]] are also passed through this method. */ - private def onReceiveMessage(header: MessageHeader, body: MessageBody, device: Device.ID): Unit = { - assert(header.Origin != Crypto.getLocalAddress) + private def onReceiveMessage(message: Message, device: Device.ID): Unit = { + assert(message.Header.Origin != Crypto.getLocalAddress) - body match { + message.Body match { case info: ConnectionInfo => - if (header.Origin == Crypto.getLocalAddress) + if (message.Header.Origin == Crypto.getLocalAddress) return onNeighborConnected(info, device) - case data: Data => - val up = new ScalaMessagePack().createBufferUnpacker(data.data) - val encrypted = up.readByteArray() - val key = up.readByteArray() - val (message, signature) = Message.read(Crypto.decrypt(encrypted, key)) - - if (!Crypto.isValidSignature(message, signature)) { - Log.i(Tag, "Dropping message with invalid signature from " + header.Origin) + case _ => + val decrypted = Crypto.decrypt(message) + if (!Crypto.verify(decrypted)) { + Log.i(Tag, "Dropping message with invalid signature from " + message.Header.Origin) return } - Database.addMessage(message) - MainHandler.post(new Runnable { - override def run(): Unit = { - messageListeners.foreach(l => - if (l.get != null) - l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message) - else - messageListeners -= l) - } - }) + callMessageReceivedListeners(decrypted) + Database.addMessage(decrypted) } } + /** + * Calls all [[OnMessageReceivedListener]]s with the new message. + */ + private def callMessageReceivedListeners(message: Message): Unit = { + MainHandler.post(new Runnable { + override def run(): Unit = { + messageListeners.foreach(l => + if (l.get != null) + l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message) + else + messageListeners -= l) + } + }) + } + /** * Called when a [[ConnectionInfo]] message from a new neighbor is received. */ private def onNeighborConnected(info: ConnectionInfo, device: Device.ID): Unit = { val sender = Crypto.calculateAddress(info.key) if (sender == Address.Broadcast || sender == Address.Null) { - connections(device).close() + Log.i(Tag, "Received ConnectionInfo message with invalid sender " + sender + ", ignoring") return } diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala index f3e9d4b..967574d 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala @@ -22,7 +22,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un Socket.connect() } catch { case e: IOException => - Log.w(Tag, "Failed to connect to " + device.toString, e) + Log.v(Tag, "Failed to connect to " + device.toString, e) try { Socket.close() } catch { diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala index 0fd95bd..750a134 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala @@ -41,6 +41,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter, } val device: Device = new Device(socket.getRemoteDevice, true) + Log.i(Tag, "Incoming connection from " + device.toString) onConnected(device, socket) } } diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala index 4761de1..2126efe 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala @@ -1,7 +1,6 @@ package com.nutomic.ensichat.bluetooth import java.io._ -import java.util.Date import android.bluetooth.BluetoothSocket import android.util.Log @@ -18,7 +17,7 @@ import com.nutomic.ensichat.messages.Crypto * @param onReceive Called when a message was received from the other device. */ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatService, - crypto: Crypto, onReceive: (MessageHeader, MessageBody, Device.ID) => Unit) + crypto: Crypto, onReceive: (Message, Device.ID) => Unit) extends Thread { private val Tag: String = "TransferThread" @@ -44,26 +43,16 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi override def run(): Unit = { Log.i(Tag, "Starting data transfer with " + device.toString) - send(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit, new Date(), Address.Null, - Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey)) + send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit, + Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey)))) while (socket.isConnected) { try { - val headerBytes = new Array[Byte](MessageHeader.Length) - InStream.read(headerBytes, 0, MessageHeader.Length) - val header = MessageHeader.read(headerBytes) - val bodyLength = (header.Length - MessageHeader.Length).toInt + if (InStream.available() > 0) { + val msg = Message.read(InStream) - val bodyBytes = new Array[Byte](bodyLength) - InStream.read(bodyBytes, 0, bodyLength) - - val body = - header.MessageType match { - case ConnectionInfo.Type => ConnectionInfo.read(bodyBytes) - case Data.Type => Data.read(bodyBytes) - } - - onReceive(header, body, device.Id) + onReceive(msg, device.Id) + } } catch { case e: RuntimeException => Log.i(Tag, "Received invalid message", e) @@ -73,11 +62,12 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi } } service.onConnectionChanged(new Device(device.bluetoothDevice, false), null) + Log.i(Tag, "Neighbor " + device + " has disconnected") } - def send(header: MessageHeader, body: MessageBody): Unit = { + def send(msg: Message): Unit = { try { - OutStream.write(header.write(body)) + OutStream.write(msg.write) } catch { case e: IOException => Log.e(Tag, "Failed to write message", e) } @@ -85,6 +75,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi def close(): Unit = { try { + Log.i(Tag, "Closing connection to " + device) socket.close() } catch { case e: IOException => Log.e(Tag, "Failed to close socket", e); diff --git a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala b/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala index 49669cf..85423e0 100644 --- a/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala +++ b/app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala @@ -1,7 +1,5 @@ package com.nutomic.ensichat.fragments -import java.util.Date - import android.app.ListFragment import android.os.Bundle import android.view.View.OnClickListener @@ -11,10 +9,9 @@ import android.widget.TextView.OnEditorActionListener import android.widget._ import com.nutomic.ensichat.R import com.nutomic.ensichat.activities.EnsiChatActivity -import com.nutomic.ensichat.aodvv2.Address +import com.nutomic.ensichat.aodvv2.{Address, Message, Text} import com.nutomic.ensichat.bluetooth.ChatService import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener -import com.nutomic.ensichat.messages.{Crypto, Message, TextMessage} import com.nutomic.ensichat.util.MessagesAdapter import scala.collection.SortedSet @@ -43,7 +40,7 @@ class ChatFragment extends ListFragment with OnClickListener private var listView: ListView = _ - private var adapter: ArrayAdapter[TextMessage] = _ + private var adapter: ArrayAdapter[Message] = _ override def onActivityCreated(savedInstanceState: Bundle): Unit = { super.onActivityCreated(savedInstanceState) @@ -103,10 +100,8 @@ class ChatFragment extends ListFragment with OnClickListener case R.id.send => val text = messageText.getText.toString.trim if (!text.isEmpty) { - val message = - new TextMessage(Crypto.getLocalAddress(getActivity), address, new Date(), text.toString) - chatService.send(message) - adapter.add(message) + val message = new Text(text.toString) + chatService.sendTo(address, message) messageText.getText.clear() } } @@ -115,9 +110,8 @@ class ChatFragment extends ListFragment with OnClickListener * Displays new messages in UI. */ override def onMessageReceived(messages: SortedSet[Message]): Unit = { - messages.filter(m => m.sender == address || m.receiver == address) - .filter(_.isInstanceOf[TextMessage]) - .foreach(m => adapter.add(m.asInstanceOf[TextMessage])) + messages.filter(m => Set(m.Header.Origin, m.Header.Target).contains(address)) + .foreach(adapter.add) } /** diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala b/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala index 515d345..3706823 100644 --- a/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala +++ b/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala @@ -9,7 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey} import android.content.Context import android.preference.PreferenceManager import android.util.Log -import com.nutomic.ensichat.aodvv2.Address +import com.nutomic.ensichat.aodvv2._ import com.nutomic.ensichat.messages.Crypto._ import com.nutomic.ensichat.util.PRNGFixes @@ -107,6 +107,7 @@ class Crypto(Context: Context) { * * @throws RuntimeException If the key does not exist. */ + @throws[RuntimeException] def getPublicKey(address: Address): PublicKey = { loadKey(address.toString, classOf[PublicKey]) } @@ -114,49 +115,32 @@ class Crypto(Context: Context) { /** * Adds a new public key for a remote device. * - * If a key for the device already exists, nothing is done. - * - * @param address The device to which the key belongs. - * @param key The new key to add. + * @throws RuntimeException If a this key */ + @throws[RuntimeException] def addPublicKey(address: Address, key: PublicKey): Unit = { - if (!havePublicKey(address)) { - saveKey(address.toString, key) - } else { - Log.i(Tag, "Already have key for " + address.toString + ", not overwriting") - } + if (havePublicKey(address)) + throw new RuntimeException("Already have key for " + address + ", not overwriting") + + saveKey(address.toString, key) } - /** - * Checks if the message was properly signed. - * - * This is done by signing the output of [[Message.write()]] called with an empty signature. - * - * @param message The message to verify. - * @param signature The signature that was sent - * @return True if the signature is valid. - */ - def isValidSignature(message: Message, signature: Array[Byte], key: PublicKey = null): Boolean = { - val publicKey = - if (key != null) key - else loadKey(message.sender.toString, classOf[PublicKey]) - val sig = Signature.getInstance(SignAlgorithm) - sig.initVerify(publicKey) - sig.update(message.write(Array[Byte]())) - sig.verify(signature) - } - - /** - * Returns a cryptographic signature for the given message (using local private key). - * - * This is done by signing the output of [[Message.write()]] called with an empty signature. - */ - def calculateSignature(message: Message): Array[Byte] = { + def sign(msg: Message): Message = { val sig = Signature.getInstance(SignAlgorithm) val key = loadKey(PrivateKeyAlias, classOf[PrivateKey]) sig.initSign(key) - sig.update(message.write(Array[Byte]())) - sig.sign + 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]) + val sig = Signature.getInstance(SignAlgorithm) + sig.initVerify(publicKey) + sig.update(msg.Body.write) + sig.verify(msg.Crypto.Signature.get) } /** @@ -239,50 +223,42 @@ class Crypto(Context: Context) { */ private def keyFolder = new File(Context.getFilesDir, "keys") - /** - * Encrypts data for the given receiver. - * - * @param receiver The device that should be able to decrypt this message. - * @param data The message to encrypt. - * @param key Optional RSA public key to use for encryption. - * @return Pair of AES encrypted data and RSA encrypted AES key. - */ - def encrypt(receiver: Address, data: Array[Byte], key: PublicKey = null): - (Array[Byte], Array[Byte]) = { + def encrypt(msg: Message, key: PublicKey = null): Message = { + 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 encryptedData = copyThroughCipher(symmetricCipher, data) + val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.Body.write)) // Asymmetric encryption of secret key val publicKey = if (key != null) key - else loadKey(receiver.toString, classOf[PublicKey]) + else loadKey(msg.Header.Target.toString, classOf[PublicKey]) val asymmetricCipher = Cipher.getInstance(KeyAlgorithm) asymmetricCipher.init(Cipher.WRAP_MODE, publicKey) - (encryptedData, asymmetricCipher.wrap(secretKey)) + new Message(msg.Header, + new CryptoData(msg.Crypto.Signature, Option(asymmetricCipher.wrap(secretKey))), encrypted) } - /** - * Decrypts the output of [[encrypt]]. - * - * @param data The AES encrypted data to decrypt. - * @param key The RSA encrypted AES key used to encrypt data. - * @return The plain text data. - */ - def decrypt(data: Array[Byte], key: Array[Byte]): Array[Byte] = { + 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 secretKey = asymmetricCipher.unwrap(key, 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, secretKey) - val dec = copyThroughCipher(symmetricCipher, data) - dec + symmetricCipher.init(Cipher.DECRYPT_MODE, key) + val decryped = copyThroughCipher(symmetricCipher, msg.Body.asInstanceOf[EncryptedBody].Data) + val body = msg.Header.MessageType match { + case RequestAddContact.Type => RequestAddContact.read(decryped) + case ResultAddContact.Type => ResultAddContact.read(decryped) + case Text.Type => Text.read(decryped) + } + new Message(msg.Header, msg.Crypto, body) } /** diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/Message.scala b/app/src/main/scala/com/nutomic/ensichat/messages/Message.scala deleted file mode 100644 index 715c8ba..0000000 --- a/app/src/main/scala/com/nutomic/ensichat/messages/Message.scala +++ /dev/null @@ -1,122 +0,0 @@ -package com.nutomic.ensichat.messages - -import java.io.IOException -import java.util.{Date, Objects} - -import com.nutomic.ensichat.aodvv2.Address -import org.msgpack.ScalaMessagePack -import org.msgpack.packer.Packer - -object Message { - - /** - * Types of messages that can be transfered. - * - * There must be one type for each implementation and vice versa. - */ - object Type { - val Text = 1 - val RequestAddContact = 2 - val ResultAddContact = 3 - } - - /** - * Orders messages by date, oldest messages first. - */ - val Ordering = new Ordering[Message] { - override def compare(m1: Message, m2: Message) = m1.date.compareTo(m2.date) - } - - /** - * Reads a byte array that was written by [[Message.write]] into the correct - * message implementation.. - * - * @return Deserialized message and sits signature. - */ - def read(bytes: Array[Byte]): (Message, Array[Byte]) = { - val up = new ScalaMessagePack().createBufferUnpacker(bytes) - - @throws[IOException]("If the message can't be parsed") - @throws[RuntimeException]("If the message has an unknown type") - val messageType = up.readInt() - val sender = new Address(up.readByteArray()) - val receiver = new Address(up.readByteArray()) - val date = new Date(up.readLong()) - val sig = up.readByteArray() - (messageType match { - case Type.Text => TextMessage.read(sender, receiver, date, up) - case Type.RequestAddContact => RequestAddContactMessage.read(sender, receiver, date, up) - case Type.ResultAddContact => ResultAddContactMessage.read(sender, receiver, date, up) - case t => - throw new RuntimeException("Received message of unknown type " + t) - }, sig) - } - -} - -/** - * Message object that can be sent between remote devices. - * - * @param messageType One of [[Message.Type]]. - */ -@Deprecated -abstract class Message(val messageType: Int) { - - /** - * Device where the message was sent from. - */ - val sender: Address - - /** - * Device the message is addressed to. - */ - val receiver: Address - - /** - * Timestamp of message creation. - */ - val date: Date - - /** - * Writes this message and the given signature into byte array. - * - * Signature may not be null, but can be an empty array. - */ - def write(signature: Array[Byte]): Array[Byte] = { - val packer = new ScalaMessagePack().createBufferPacker() - packer.write(messageType) - .write(sender.Bytes) - .write(receiver.Bytes) - .write(date.getTime) - .write(signature) - doWrite(packer) - packer.toByteArray - } - - /** - * Serializes any extra data for implementing classes. - */ - protected def doWrite(packer: Packer): Unit - - /** - * Returns true if objects are equal. - * - * Implementations must provide their own implementation to check the result of this - * function and their own data. - */ - override def equals(a: Any): Boolean = a match { - case o: Message => sender == o.sender && receiver == o.receiver && date == o.date - case _ => false - } - - /** - * Returns a hash code for this object. - * - * Implementations must provide their own implementation to check the result of this - * function and their own data. - */ - override def hashCode: Int = Objects.hash(sender, receiver, date) - - override def toString: String - -} diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/RequestAddContactMessage.scala b/app/src/main/scala/com/nutomic/ensichat/messages/RequestAddContactMessage.scala deleted file mode 100644 index 000a261..0000000 --- a/app/src/main/scala/com/nutomic/ensichat/messages/RequestAddContactMessage.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.nutomic.ensichat.messages - -import java.util.Date - -import com.nutomic.ensichat.activities.AddContactsActivity -import com.nutomic.ensichat.aodvv2.Address -import com.nutomic.ensichat.messages.Message._ -import org.msgpack.packer.Packer -import org.msgpack.unpacker.Unpacker - -object RequestAddContactMessage { - - def read(sender: Address, receiver: Address, date: Date, up: Unpacker) = - new RequestAddContactMessage(sender, receiver, date) - -} - -/** - * Message sent by [[AddContactsActivity]] to notify a device that it should be added as a contact. - */ -class RequestAddContactMessage(override val sender: Address, override val receiver: Address, - override val date: Date) extends Message(Type.RequestAddContact) { - - override def doWrite(packer: Packer) = { - } - - override def toString = "RequestAddContactMessage(" + sender.toString + ", " + receiver.toString + - ", " + date.toString + ")" - -} diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/ResultAddContactMessage.scala b/app/src/main/scala/com/nutomic/ensichat/messages/ResultAddContactMessage.scala deleted file mode 100644 index cf8e04b..0000000 --- a/app/src/main/scala/com/nutomic/ensichat/messages/ResultAddContactMessage.scala +++ /dev/null @@ -1,37 +0,0 @@ -package com.nutomic.ensichat.messages - -import java.util.{Date, Objects} - -import com.nutomic.ensichat.activities.AddContactsActivity -import com.nutomic.ensichat.aodvv2.Address -import com.nutomic.ensichat.messages.Message._ -import org.msgpack.packer.Packer -import org.msgpack.unpacker.Unpacker - -object ResultAddContactMessage { - - def read(sender: Address, receiver: Address, date: Date, up: Unpacker) = - new ResultAddContactMessage(sender, receiver, date, up.readBoolean()) - -} - -/** - * Message sent by [[AddContactsActivity]] to tell a device whether the user confirmed adding it - * to contacts. - */ -class ResultAddContactMessage(override val sender: Address, override val receiver: Address, - override val date: Date, val Accepted: Boolean) - extends Message(Type.ResultAddContact) { - - override def doWrite(packer: Packer) = packer.write(Accepted) - - override def equals(a: Any) = - super.equals(a) && a.asInstanceOf[ResultAddContactMessage].Accepted == Accepted - - override def hashCode = - Objects.hash(super.hashCode: java.lang.Integer, Accepted: java.lang.Boolean) - - override def toString = "ResultAddContactMessage(" + sender.toString + ", " + receiver.toString + - ", " + date.toString + ", " + Accepted + ")" - -} diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/TextMessage.scala b/app/src/main/scala/com/nutomic/ensichat/messages/TextMessage.scala deleted file mode 100644 index e9887c6..0000000 --- a/app/src/main/scala/com/nutomic/ensichat/messages/TextMessage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.nutomic.ensichat.messages - -import java.util.{Date, Objects} - -import com.nutomic.ensichat.aodvv2.Address -import com.nutomic.ensichat.messages.Message._ -import org.msgpack.packer.Packer -import org.msgpack.unpacker.Unpacker - -object TextMessage { - - def read(sender: Address, receiver: Address, date: Date, up: Unpacker): TextMessage = - new TextMessage(sender, receiver, date, up.readString()) - -} - -/** - * Message that contains text. - */ -class TextMessage(override val sender: Address, override val receiver: Address, - override val date: Date, val text: String) extends Message(Type.Text) { - - override def doWrite(packer: Packer) = packer.write(text) - - override def equals(a: Any) = super.equals(a) && a.asInstanceOf[TextMessage].text == text - - override def hashCode = Objects.hash(super.hashCode: java.lang.Integer, text) - - override def toString = "TextMessage(" + sender.toString + ", " + receiver.toString + - ", " + date.toString + ", " + text + ")" - -} diff --git a/app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala b/app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala index 78ad102..df8cb51 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala @@ -2,6 +2,9 @@ package com.nutomic.ensichat.util import java.nio.ByteBuffer +/** + * Provides various helper methods for [[ByteBuffer]]. + */ object BufferUtils { def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort @@ -25,6 +28,6 @@ object BufferUtils { b } - def toString(bb: ByteBuffer)= bb.array().slice(0, 4).map("%02X" format _).mkString + def toString(array: Array[Byte]) = array.map("%02X".format(_)).mkString } \ No newline at end of file diff --git a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala b/app/src/main/scala/com/nutomic/ensichat/util/Database.scala index a464a3a..d7c005e 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/Database.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/Database.scala @@ -4,8 +4,7 @@ import java.util.Date import android.content.{ContentValues, Context} import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper} -import com.nutomic.ensichat.aodvv2.Address -import com.nutomic.ensichat.messages._ +import com.nutomic.ensichat.aodvv2._ import scala.collection.SortedSet import scala.collection.immutable.TreeSet @@ -18,8 +17,8 @@ object Database { private val CreateMessagesTable = "CREATE TABLE messages(" + "_id integer primary key autoincrement," + - "sender text not null," + - "receiver text not null," + + "origin text not null," + + "target text not null," + "text text not null," + "date integer not null);" // Unix timestamp of message. @@ -35,8 +34,6 @@ object Database { class Database(context: Context) extends SQLiteOpenHelper(context, Database.DatabaseName, null, Database.DatabaseVersion) { - private val Tag = "MessageStore" - private var contactsUpdatedListeners = Set[() => Unit]() override def onCreate(db: SQLiteDatabase): Unit = { @@ -49,17 +46,21 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data */ def getMessages(address: Address, count: Int): SortedSet[Message] = { val c = getReadableDatabase.query(true, - "messages", Array("sender", "receiver", "text", "date"), - "sender = ? OR receiver = ?", Array(address.toString, address.toString), + "messages", Array("origin", "target", "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 m = new TextMessage( - new Address(c.getString(c.getColumnIndex("sender"))), - new Address(c.getString(c.getColumnIndex("receiver"))), - new Date(c.getLong(c.getColumnIndex("date"))), - new String(c.getString(c.getColumnIndex ("text")))) - messages += m + val header = new MessageHeader( + Text.Type, + -1, + new Address(c.getString(c.getColumnIndex("origin"))), + new Address(c.getString(c.getColumnIndex("target"))), + -1, + -1, + new Date(c.getLong(c.getColumnIndex("date")))) + val body = new Text(new String(c.getString(c.getColumnIndex ("text")))) + messages += new Message(header, body) } c.close() messages @@ -68,16 +69,16 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data /** * Inserts the given new message into the database. */ - def addMessage(message: Message): Unit = message match { - case msg: TextMessage => + def addMessage(message: Message): Unit = message.Body match { + case msg: Text => val cv = new ContentValues() - cv.put("sender", msg.sender.toString) - cv.put("receiver", msg.receiver.toString) + cv.put("origin", message.Header.Origin.toString) + cv.put("target", message.Header.Target.toString) // toString used as workaround for compile error with Long. - cv.put("date", msg.date.getTime.toString) + cv.put("date", message.Header.Time.getTime.toString) cv.put("text", msg.text) getWritableDatabase.insert("messages", null, cv) - case _: RequestAddContactMessage | _: ResultAddContactMessage => + case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact => // Never stored. } diff --git a/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala b/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala index 5ed39be..035dea2 100644 --- a/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala +++ b/app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala @@ -4,14 +4,13 @@ import android.content.Context import android.view.{Gravity, View, ViewGroup} import android.widget.{ArrayAdapter, RelativeLayout, TextView} import com.nutomic.ensichat.R -import com.nutomic.ensichat.aodvv2.Address -import com.nutomic.ensichat.messages.TextMessage +import com.nutomic.ensichat.aodvv2.{Address, Message, Text} /** - * Displays [[TextMessage]]s in ListView. + * Displays [[Message]]s in ListView. */ class MessagesAdapter(context: Context, remoteAddress: Address) extends - ArrayAdapter[TextMessage](context, R.layout.item_message, android.R.id.text1) { + ArrayAdapter[Message](context, R.layout.item_message, android.R.id.text1) { /** * Free space to the right/left to a message depending on who sent it, in dip. @@ -22,11 +21,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).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).sender != remoteAddress) { + if (getItem(position).Header.Origin != remoteAddress) { view.setGravity(Gravity.RIGHT) lp.setMargins(margin, 0, 0, 0) } else { diff --git a/build.gradle b/build.gradle index 996243c..216ac9a 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:0.14.2' - classpath 'com.github.ben-manes:gradle-versions-plugin:0.5' + classpath 'com.github.ben-manes:gradle-versions-plugin:0.6' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/gradle/wrapper/gradle-wrapper.jar b/gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..3d0dee6 Binary files /dev/null and b/gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/gradle/wrapper/gradle-wrapper.properties b/gradle/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..67123dd --- /dev/null +++ b/gradle/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Dec 04 22:57:09 EET 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip diff --git a/gradle/gradlew b/gradle/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradle/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradle/gradlew.bat b/gradle/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradle/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/gradle/local.properties b/gradle/local.properties new file mode 100644 index 0000000..66280f3 --- /dev/null +++ b/gradle/local.properties @@ -0,0 +1,11 @@ +## This file is automatically generated by Android Studio. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Thu Dec 04 22:57:09 EET 2014 +sdk.dir=/home/felix/software/android-sdk diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64..3d0dee6 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 33afa6f..9ffe42a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Dec 09 17:41:26 EET 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip