Browse Source

Moved remaining messages to binary encoding, removed msgpack dependency.

This seems to have added some random disconnects, but there doesn't
seem to be a reason for this in the code.
send-bitcoin
Felix Ableitner 8 years ago
parent
commit
834f3ca724
  1. 112
      PROTOCOL.md
  2. 25
      app/build.gradle
  3. 7
      app/proguard-rules.pro
  4. 7
      app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/AddressTest.scala
  5. 35
      app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageHeaderTest.scala
  6. 77
      app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/MessageTest.scala
  7. 18
      app/src/androidTest/scala/com/nutomic/ensichat/aodvv2/ResultAddContactTest.scala
  8. 20
      app/src/androidTest/scala/com/nutomic/ensichat/messages/CryptoTest.scala
  9. 50
      app/src/androidTest/scala/com/nutomic/ensichat/messages/MessageTest.scala
  10. 22
      app/src/androidTest/scala/com/nutomic/ensichat/util/DatabaseTest.scala
  11. 2
      app/src/main/res/xml/settings.xml
  12. 41
      app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala
  13. 13
      app/src/main/scala/com/nutomic/ensichat/aodvv2/ConnectionInfo.scala
  14. 67
      app/src/main/scala/com/nutomic/ensichat/aodvv2/CryptoData.scala
  15. 37
      app/src/main/scala/com/nutomic/ensichat/aodvv2/Data.scala
  16. 15
      app/src/main/scala/com/nutomic/ensichat/aodvv2/EncryptedBody.scala
  17. 50
      app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala
  18. 7
      app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageBody.scala
  19. 18
      app/src/main/scala/com/nutomic/ensichat/aodvv2/MessageHeader.scala
  20. 34
      app/src/main/scala/com/nutomic/ensichat/aodvv2/RequestAddContact.scala
  21. 41
      app/src/main/scala/com/nutomic/ensichat/aodvv2/ResultAddContact.scala
  22. 50
      app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala
  23. 115
      app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala
  24. 2
      app/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala
  25. 1
      app/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala
  26. 31
      app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala
  27. 18
      app/src/main/scala/com/nutomic/ensichat/fragments/ChatFragment.scala
  28. 98
      app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala
  29. 122
      app/src/main/scala/com/nutomic/ensichat/messages/Message.scala
  30. 30
      app/src/main/scala/com/nutomic/ensichat/messages/RequestAddContactMessage.scala
  31. 37
      app/src/main/scala/com/nutomic/ensichat/messages/ResultAddContactMessage.scala
  32. 32
      app/src/main/scala/com/nutomic/ensichat/messages/TextMessage.scala
  33. 5
      app/src/main/scala/com/nutomic/ensichat/util/BufferUtils.scala
  34. 41
      app/src/main/scala/com/nutomic/ensichat/util/Database.scala
  35. 11
      app/src/main/scala/com/nutomic/ensichat/util/MessagesAdapter.scala
  36. 2
      build.gradle
  37. BIN
      gradle/gradle/wrapper/gradle-wrapper.jar
  38. 6
      gradle/gradle/wrapper/gradle-wrapper.properties
  39. 164
      gradle/gradlew
  40. 90
      gradle/gradlew.bat
  41. 11
      gradle/local.properties
  42. BIN
      gradle/wrapper/gradle-wrapper.jar
  43. 3
      gradle/wrapper/gradle-wrapper.properties

112
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Data (variable length) \
/ /
### 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.
Length is the number of bytes in data.
### Text (Type = 6)
Data is any binary data that should be transported.
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Text (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
This message type is deprecated.
Text the string to be transferred, encoded as UTF-8.

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

7
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

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

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

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

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

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

50
app/src/androidTest/scala/com/nutomic/ensichat/messages/MessageTest.scala

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

22
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 = {

2
app/src/main/res/xml/settings.xml

@ -5,7 +5,7 @@
<EditTextPreference
android:title="@string/scan_interval_seconds"
android:key="scan_interval_seconds"
android:defaultValue="5"
android:defaultValue="15"
android:inputType="number"
android:numeric="integer" />

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

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

67
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) + ")"
}

37
app/src/main/scala/com/nutomic/ensichat/aodvv2/Data.scala

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

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

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

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

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

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

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

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

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

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

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

31
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);

18
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