Use key fingerprints instead of bluetooth addresses.
This commit is contained in:
parent
5f123afcab
commit
de86f5f121
33 changed files with 840 additions and 347 deletions
133
PROTOCOL.md
Normal file
133
PROTOCOL.md
Normal file
|
@ -0,0 +1,133 @@
|
|||
Introduction and Definitions
|
||||
----------------------------
|
||||
|
||||
This protocol is used by two or more devices forming a mesh net.
|
||||
|
||||
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 address consists of 32 bytes and is the SHA-256 hash of the
|
||||
node's public key.
|
||||
|
||||
The broadcast address is
|
||||
`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`
|
||||
(i.e. all bits set).
|
||||
|
||||
The null address is
|
||||
`0x0000000000000000000000000000000000000000000000000000000000000000`
|
||||
(i.e. no bits set).
|
||||
|
||||
Nodes MUST NOT use a public key with the broadcast address or null
|
||||
address as hash (they must generate a new key pair). Also, other
|
||||
nodes MUST NOT connect to a node with either address.
|
||||
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
### Header
|
||||
|
||||
Every message starts with one 32 bit word indicating the message
|
||||
version, type and ID, followed by the length of the message. The
|
||||
header is in network byte order, i.e. big endian.
|
||||
|
||||
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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Ver | Type | Hop Limit | Hop Count |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Time |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
| Origin Address |
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
| Target Address |
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Sequence Number | Metric | Reserved |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Ver specifies the protocol version number. This is currently 0. A
|
||||
message with unknown version number MUST be ignored. The connection
|
||||
where such a packet came from MAY be closed.
|
||||
|
||||
Hop Limit SHOULD be set to `MAX_HOP_COUNT` on message creation, and
|
||||
MUST NOT be changed by a forwarding node.
|
||||
|
||||
Hop Count specifies the number of nodes a message may pass. When
|
||||
creating a package, it is initialized to 0. Whenever a node forwards
|
||||
a package, it MUST increment the hop limit by one. If the hop limit
|
||||
BEFORE/AFTER? incrementing equals Hop Limit, the package MUST be
|
||||
ignored.
|
||||
|
||||
Length is the message size in bytes, including the header.
|
||||
|
||||
Time is the unix timestamp of message creation, in seconds, as a
|
||||
signed integer.
|
||||
|
||||
Origin Address is the address of the node that initially created the
|
||||
message.
|
||||
|
||||
Target Address is the address of the node that should receive the
|
||||
message.
|
||||
|
||||
Sequence number is the sequence number of either the source or target
|
||||
node for this message, depending on type.
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
encryption when communicating with the sending node.
|
||||
|
||||
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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Key Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Key (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Key length is the size of the key in bytes.
|
||||
|
||||
Key is the public key of the sending node.
|
||||
|
||||
After this message has been received, communication with normal messages
|
||||
may start.
|
||||
|
||||
|
||||
### Data (Data Transfer, Type = 255)
|
||||
|
||||
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 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Data (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
Length is the number of bytes in data.
|
||||
|
||||
Data is any binary data that should be transported.
|
||||
|
||||
This message type is deprecated.
|
|
@ -20,9 +20,10 @@ dependencies {
|
|||
compile("org.msgpack:msgpack-scala_2.11:0.6.11") {
|
||||
transitive = false;
|
||||
}
|
||||
compile('org.msgpack:msgpack:0.6.11'){
|
||||
compile('org.msgpack:msgpack:0.6.11') {
|
||||
transitive = false;
|
||||
}
|
||||
compile 'com.google.guava:guava:18.0'
|
||||
}
|
||||
|
||||
|
||||
|
@ -50,13 +51,14 @@ android {
|
|||
|
||||
buildTypes {
|
||||
thin {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix ".debug"
|
||||
minifyEnabled false
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
applicationIdSuffix ".debug"
|
||||
minifyEnabled true
|
||||
proguardFile file("proguard-rules.pro")
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
|
|
|
@ -22,7 +22,7 @@ class MainActivityTest extends ActivityUnitTestCase[MainActivity](classOf[MainAc
|
|||
|
||||
override def bindService(service: Intent, conn: ServiceConnection, flags: Int): Boolean = false
|
||||
|
||||
override def unbindService(conn: ServiceConnection): Unit = null
|
||||
override def unbindService(conn: ServiceConnection): Unit = {}
|
||||
}
|
||||
|
||||
override def setUp(): Unit = {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package com.nutomic.ensichat.aodvv2
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.aodvv2.AddressTest._
|
||||
import junit.framework.Assert._
|
||||
|
||||
object AddressTest {
|
||||
|
||||
val a1 = new Address("A51B74475EE622C3C924DB147668F85E024CA0B44CA146B5E3D3C31A54B34C1E")
|
||||
|
||||
val a2 = new Address("222229685A73AB8F2F853B3EA515633B7CD5A6ABDC3210BC4EF38F955A14AAF6")
|
||||
|
||||
val a3 = new Address("3333359893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
class AddressTest extends AndroidTestCase {
|
||||
|
||||
def testEncode(): Unit = {
|
||||
Set(Address.Broadcast, Address.Null, a1, a2, a3, a4).foreach{a =>
|
||||
val base32 = a.toString
|
||||
val read = new Address(base32)
|
||||
assertEquals(a, read)
|
||||
assertEquals(a.hashCode, read.hashCode)
|
||||
}
|
||||
|
||||
assertEquals(a1, new Address(a1Binary))
|
||||
assertEquals(a1Binary.deep, a1.Bytes.deep)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.nutomic.ensichat.aodvv2
|
||||
|
||||
import android.content.Context
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.messages.Crypto
|
||||
import junit.framework.Assert
|
||||
|
||||
object ConnectionInfoTest {
|
||||
|
||||
def generateCi(context: Context) = {
|
||||
val crypto = new Crypto(context)
|
||||
if (!crypto.localKeysExist)
|
||||
crypto.generateLocalKeys()
|
||||
new ConnectionInfo(crypto.getLocalPublicKey)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ConnectionInfoTest extends AndroidTestCase {
|
||||
|
||||
def testWriteRead(): Unit = {
|
||||
val ci = ConnectionInfoTest.generateCi(getContext)
|
||||
val bytes = ci.write
|
||||
val body = ConnectionInfo.read(bytes)
|
||||
Assert.assertEquals(ci.key, body.asInstanceOf[ConnectionInfo].key)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.nutomic.ensichat.aodvv2
|
||||
|
||||
import java.util.Date
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import junit.framework.Assert
|
||||
|
||||
object MessageHeaderTest {
|
||||
|
||||
val h1 = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit, new Date(), AddressTest.a3,
|
||||
AddressTest.a4, 456, 123)
|
||||
|
||||
val h2 = new MessageHeader(0xfff, 0, new Date(0xffffffff), Address.Null, Address.Broadcast, 0,
|
||||
0xff)
|
||||
|
||||
val h3 = new MessageHeader(0xfff, 0xff, new Date(0), Address.Broadcast, Address.Null, 0xffff, 0)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
package com.nutomic.ensichat.messages
|
||||
|
||||
import java.util.GregorianCalendar
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.messages.MessageTest._
|
||||
import junit.framework.Assert._
|
||||
|
||||
class CryptoTest extends AndroidTestCase {
|
||||
|
||||
lazy val Crypto: Crypto = new Crypto(getContext.getFilesDir)
|
||||
lazy val Crypto: Crypto = new Crypto(getContext)
|
||||
|
||||
override def setUp(): Unit = {
|
||||
super.setUp()
|
||||
|
@ -24,14 +21,10 @@ class CryptoTest extends AndroidTestCase {
|
|||
}
|
||||
|
||||
def testEncryptDecrypt(): Unit = {
|
||||
val in = new DeviceInfoMessage(new Device.ID("DD:DD:DD:DD:DD:DD"),
|
||||
new Device.ID("CC:CC:CC:CC:CC:CC"), new GregorianCalendar(2014, 10, 31).getTime,
|
||||
Crypto.getLocalPublicKey)
|
||||
val (encrypted, key) = Crypto.encrypt(null, in.write(Array[Byte]()), Crypto.getLocalPublicKey)
|
||||
val (encrypted, key) =
|
||||
Crypto.encrypt(null, MessageTest.m1.write(Array[Byte]()), Crypto.getLocalPublicKey)
|
||||
val decrypted = Crypto.decrypt(encrypted, key)
|
||||
val out = Message.read(decrypted)._1.asInstanceOf[DeviceInfoMessage]
|
||||
|
||||
assertEquals(in, out)
|
||||
assertEquals(MessageTest.m1, Message.read(decrypted)._1)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import java.io.{PipedInputStream, PipedOutputStream}
|
|||
import java.util.GregorianCalendar
|
||||
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.aodvv2.AddressTest
|
||||
import com.nutomic.ensichat.messages.MessageTest._
|
||||
import junit.framework.Assert._
|
||||
|
||||
|
@ -12,13 +12,13 @@ import scala.collection.immutable.TreeSet
|
|||
|
||||
object MessageTest {
|
||||
|
||||
val m1 = new TextMessage(new Device.ID("AA:AA:AA:AA:AA:AA"), new Device.ID("BB:BB:BB:BB:BB:BB"),
|
||||
val m1 = new TextMessage(AddressTest.a1, AddressTest.a2,
|
||||
new GregorianCalendar(2014, 10, 29).getTime, "first")
|
||||
|
||||
val m2 = new TextMessage(new Device.ID("AA:AA:AA:AA:AA:AA"), new Device.ID("CC:CC:CC:CC:CC:CC"),
|
||||
val m2 = new TextMessage(AddressTest.a1, AddressTest.a3,
|
||||
new GregorianCalendar(2014, 10, 30).getTime, "second")
|
||||
|
||||
val m3 = new TextMessage(new Device.ID("DD:DD:DD:DD:DD:DD"), new Device.ID("BB:BB:BB:BB:BB:BB"),
|
||||
val m3 = new TextMessage(AddressTest.a4, AddressTest.a2,
|
||||
new GregorianCalendar(2014, 10, 31).getTime, "third")
|
||||
|
||||
}
|
||||
|
@ -26,23 +26,25 @@ object MessageTest {
|
|||
class MessageTest extends AndroidTestCase {
|
||||
|
||||
def testSerialize(): Unit = {
|
||||
val pis = new PipedInputStream()
|
||||
val pos = new PipedOutputStream(pis)
|
||||
val bytes = m1.write(Array[Byte]())
|
||||
val (msg, _) = Message.read(bytes)
|
||||
assertEquals(m1, msg)
|
||||
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 += m1
|
||||
messages += m2
|
||||
assertEquals(m1, messages.firstKey)
|
||||
messages += MessageTest.m1
|
||||
messages += MessageTest.m2
|
||||
assertEquals(MessageTest.m1, messages.firstKey)
|
||||
|
||||
messages = new TreeSet[Message]()(Message.Ordering)
|
||||
messages += m2
|
||||
messages += m3
|
||||
assertEquals(m2, messages.firstKey)
|
||||
messages += MessageTest.m2
|
||||
messages += MessageTest.m3
|
||||
assertEquals(MessageTest.m2, messages.firstKey)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import android.database.DatabaseErrorHandler
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.test.AndroidTestCase
|
||||
import android.test.mock.MockContext
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.messages.MessageTest._
|
||||
import com.nutomic.ensichat.aodvv2.AddressTest
|
||||
import com.nutomic.ensichat.messages.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(m1)
|
||||
Database.addMessage(m2)
|
||||
Database.addMessage(m3)
|
||||
Database.addMessage(MessageTest.m1)
|
||||
Database.addMessage(MessageTest.m2)
|
||||
Database.addMessage(MessageTest.m3)
|
||||
}
|
||||
|
||||
override def tearDown(): Unit = {
|
||||
|
@ -38,43 +38,38 @@ class DatabaseTest extends AndroidTestCase {
|
|||
}
|
||||
|
||||
def testMessageCount(): Unit = {
|
||||
val msg1 = Database.getMessages(m1.sender, 1)
|
||||
val msg1 = Database.getMessages(MessageTest.m1.sender, 1)
|
||||
assertEquals(1, msg1.size)
|
||||
|
||||
val msg2 = Database.getMessages(m1.sender, 3)
|
||||
val msg2 = Database.getMessages(MessageTest.m1.sender, 3)
|
||||
assertEquals(2, msg2.size)
|
||||
}
|
||||
|
||||
def testMessageOrder(): Unit = {
|
||||
val msg = Database.getMessages(m1.receiver, 1)
|
||||
assertTrue(msg.contains(m3))
|
||||
val msg = Database.getMessages(MessageTest.m1.receiver, 1)
|
||||
assertTrue(msg.contains(MessageTest.m3))
|
||||
}
|
||||
|
||||
def testMessageSelect(): Unit = {
|
||||
val msg = Database.getMessages(m1.receiver, 2)
|
||||
assertTrue(msg.contains(m1))
|
||||
assertTrue(msg.contains(m3))
|
||||
val msg = Database.getMessages(MessageTest.m1.receiver, 2)
|
||||
assertTrue(msg.contains(MessageTest.m1))
|
||||
assertTrue(msg.contains(MessageTest.m3))
|
||||
}
|
||||
|
||||
def testAddContact(): Unit = {
|
||||
val device = new Device(m1.sender, "device", false)
|
||||
Database.addContact(device)
|
||||
assertTrue(Database.isContact(device.Id))
|
||||
Database.addContact(AddressTest.a1)
|
||||
assertTrue(Database.isContact(AddressTest.a1))
|
||||
val contacts = Database.getContacts
|
||||
assertEquals(1, contacts.size)
|
||||
contacts.foreach {d =>
|
||||
assertEquals(device.Name, d.Name)
|
||||
assertEquals(device.Id, d.Id)
|
||||
}
|
||||
contacts.foreach{assertEquals(AddressTest.a1, _)}
|
||||
}
|
||||
|
||||
def testAddContactCallback(): Unit = {
|
||||
val device = new Device(m1.sender, "device", false)
|
||||
var latch = new CountDownLatch(1)
|
||||
val latch = new CountDownLatch(1)
|
||||
Database.runOnContactsUpdated(() => {
|
||||
latch.countDown()
|
||||
})
|
||||
Database.addContact(device)
|
||||
Database.addContact(AddressTest.a1)
|
||||
latch.await()
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,6 @@
|
|||
<string name="exit">Exit</string>
|
||||
|
||||
|
||||
<!-- ChatFragment -->
|
||||
|
||||
<!-- Toast shown when trying to send a message to an offline contact -->
|
||||
<string name="contact_offline_toast">Contact is offline, message not sent</string>
|
||||
|
||||
|
||||
<!-- AddContactsActivity -->
|
||||
|
||||
<!-- Activity title -->
|
||||
|
|
|
@ -12,8 +12,9 @@ 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.bluetooth.ChatService
|
||||
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
||||
import com.nutomic.ensichat.bluetooth.{ChatService, Device}
|
||||
import com.nutomic.ensichat.messages.{Crypto, Message, RequestAddContactMessage, ResultAddContactMessage}
|
||||
import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
|
||||
|
||||
|
@ -24,7 +25,7 @@ import scala.collection.SortedSet
|
|||
*
|
||||
* Adding a contact requires confirmation on both sides.
|
||||
*/
|
||||
class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnectionChangedListener
|
||||
class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyContactsChangedListener
|
||||
with OnItemClickListener with OnMessageReceivedListener {
|
||||
|
||||
private val Tag = "AddContactsActivity"
|
||||
|
@ -33,12 +34,12 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
|||
|
||||
private lazy val Database = service.database
|
||||
|
||||
private lazy val Crypto = new Crypto(this.getFilesDir)
|
||||
private lazy val Crypto = new Crypto(this)
|
||||
|
||||
/**
|
||||
* Map of devices that should be added.
|
||||
*/
|
||||
private var currentlyAdding = Map[Device.ID, AddContactInfo]()
|
||||
private var currentlyAdding = Map[Address, AddContactInfo]()
|
||||
.withDefaultValue(new AddContactInfo(false, false))
|
||||
|
||||
/**
|
||||
|
@ -72,12 +73,11 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
|||
/**
|
||||
* Displays newly connected devices in the list.
|
||||
*/
|
||||
override def onConnectionChanged(devices: Map[Device.ID, Device]): Unit = {
|
||||
val filtered = devices.filter{ case (_, d) => d.Connected }
|
||||
override def onNearbyContactsChanged(devices: Set[Address]): Unit = {
|
||||
runOnUiThread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
Adapter.clear()
|
||||
filtered.values.foreach(f => Adapter.add(f))
|
||||
devices.foreach(Adapter.add)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -86,34 +86,34 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
|||
* Initiates adding the device as contact if it hasn't been added yet.
|
||||
*/
|
||||
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
|
||||
val device = Adapter.getItem(position)
|
||||
if (Database.isContact(device.Id)) {
|
||||
val address = Adapter.getItem(position)
|
||||
if (Database.isContact(address)) {
|
||||
Toast.makeText(this, R.string.contact_already_added, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
service.send(new RequestAddContactMessage(service.localDeviceId, device.Id, new Date()))
|
||||
addDeviceDialog(device)
|
||||
service.send(new RequestAddContactMessage(Crypto.getLocalAddress, address, new Date()))
|
||||
addDeviceDialog(address)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog to accept/deny adding a device as a new contact.
|
||||
*/
|
||||
private def addDeviceDialog(device: Device): Unit = {
|
||||
val id = device.Id
|
||||
private def addDeviceDialog(address: Address): Unit = {
|
||||
// Listener for dialog button clicks.
|
||||
val onClick = new OnClickListener {
|
||||
override def onClick(dialogInterface: DialogInterface, i: Int): Unit = i match {
|
||||
case DialogInterface.BUTTON_POSITIVE =>
|
||||
// Local user accepted contact, update state and send info to other device.
|
||||
currentlyAdding += (id -> new AddContactInfo(currentlyAdding(id).localConfirmed, true))
|
||||
addContactIfBothConfirmed(device)
|
||||
currentlyAdding +=
|
||||
(address -> new AddContactInfo(currentlyAdding(address).localConfirmed, true))
|
||||
addContactIfBothConfirmed(address)
|
||||
service.send(
|
||||
new ResultAddContactMessage(service.localDeviceId, device.Id, new Date(), true))
|
||||
new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), true))
|
||||
case DialogInterface.BUTTON_NEGATIVE =>
|
||||
// Local user denied adding contact, send info to other device.
|
||||
service.send(
|
||||
new ResultAddContactMessage(service.localDeviceId, device.Id, new Date(), false))
|
||||
new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), false))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,15 +122,14 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
|||
|
||||
val local = view.findViewById(R.id.local_identicon).asInstanceOf[ImageView]
|
||||
local.setImageBitmap(
|
||||
IdenticonGenerator.generate(Crypto.getLocalPublicKey, (150, 150), this))
|
||||
IdenticonGenerator.generate(Crypto.getLocalAddress, (150, 150), this))
|
||||
val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView]
|
||||
remoteTitle.setText(getString(R.string.remote_fingerprint_title, device.Name))
|
||||
remoteTitle.setText(getString(R.string.remote_fingerprint_title, address))
|
||||
val remote = view.findViewById(R.id.remote_identicon).asInstanceOf[ImageView]
|
||||
remote.setImageBitmap(
|
||||
IdenticonGenerator.generate(Crypto.getPublicKey(device.Id), (150, 150), this))
|
||||
remote.setImageBitmap(IdenticonGenerator.generate(address, (150, 150), this))
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.add_contact_dialog, device.Name))
|
||||
.setTitle(getString(R.string.add_contact_dialog, address))
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.yes, onClick)
|
||||
.setNegativeButton(android.R.string.no, onClick)
|
||||
|
@ -144,18 +143,17 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
|||
* the user is in this activity.
|
||||
*/
|
||||
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
|
||||
messages.filter(_.receiver == service.localDeviceId)
|
||||
messages.filter(_.receiver == Crypto.getLocalAddress)
|
||||
.foreach{
|
||||
case m: RequestAddContactMessage =>
|
||||
Log.i(Tag, "Remote device " + m.sender + " wants to add us as a contact, showing dialog")
|
||||
val sender = getDevice(m.sender)
|
||||
addDeviceDialog(sender)
|
||||
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(getDevice(m.sender))
|
||||
addContactIfBothConfirmed(m.sender)
|
||||
} else {
|
||||
Log.i(Tag, "Remote device " + m.sender + " denied us as a contact, showing toast")
|
||||
Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
|
||||
|
@ -165,31 +163,18 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [[Device]] for a given [[Device.ID]] that is stored in the [[Adapter]].
|
||||
*/
|
||||
private def getDevice(id: Device.ID): Device = {
|
||||
// ArrayAdapter does not return the underlying array so we have to access it manually.
|
||||
for (i <- 0 until Adapter.getCount) {
|
||||
if (Adapter.getItem(i).Id == id) {
|
||||
return Adapter.getItem(i)
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Device to add was not found")
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given device to contacts if [[AddContactInfo.localConfirmed]] and
|
||||
* [[AddContactInfo.remoteConfirmed]] are true for it in [[currentlyAdding]].
|
||||
*/
|
||||
private def addContactIfBothConfirmed(device: Device): Unit = {
|
||||
val info = currentlyAdding(device.Id)
|
||||
private def addContactIfBothConfirmed(address: Address): Unit = {
|
||||
val info = currentlyAdding(address)
|
||||
if (info.localConfirmed && info.remoteConfirmed) {
|
||||
Log.i(Tag, "Adding new contact " + device.Name)
|
||||
Database.addContact(device)
|
||||
Toast.makeText(this, getString(R.string.contact_added, device.Name), Toast.LENGTH_SHORT)
|
||||
Log.i(Tag, "Adding new contact " + address.toString)
|
||||
Database.addContact(address)
|
||||
Toast.makeText(this, getString(R.string.contact_added, address.toString), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
currentlyAdding -= device.Id
|
||||
currentlyAdding -= address
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import android.os.Bundle
|
|||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.aodvv2.Address
|
||||
import com.nutomic.ensichat.fragments.{ChatFragment, ContactsFragment}
|
||||
|
||||
/**
|
||||
|
@ -19,7 +19,7 @@ class MainActivity extends EnsiChatActivity {
|
|||
|
||||
private var ContactsFragment: ContactsFragment = _
|
||||
|
||||
private var currentChat: Option[Device.ID] = None
|
||||
private var currentChat: Option[Address] = None
|
||||
|
||||
/**
|
||||
* Initializes layout, starts service and requests Bluetooth to be discoverable.
|
||||
|
@ -38,7 +38,7 @@ class MainActivity extends EnsiChatActivity {
|
|||
ContactsFragment = fm.getFragment(savedInstanceState, classOf[ContactsFragment].getName)
|
||||
.asInstanceOf[ContactsFragment]
|
||||
if (savedInstanceState.containsKey("current_chat")) {
|
||||
currentChat = Option(new Device.ID(savedInstanceState.getString("current_chat")))
|
||||
currentChat = Option(new Address(savedInstanceState.getByteArray("current_chat")))
|
||||
openChat(currentChat.get)
|
||||
}
|
||||
} else {
|
||||
|
@ -55,7 +55,7 @@ class MainActivity extends EnsiChatActivity {
|
|||
override def onSaveInstanceState(outState: Bundle): Unit = {
|
||||
super.onSaveInstanceState(outState)
|
||||
getFragmentManager.putFragment(outState, classOf[ContactsFragment].getName, ContactsFragment)
|
||||
currentChat.collect{case c => outState.putString("current_chat", c.toString)}
|
||||
currentChat.collect{case c => outState.putByteArray("current_chat", c.Bytes)}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,12 +74,12 @@ class MainActivity extends EnsiChatActivity {
|
|||
/**
|
||||
* Opens a chat fragment for the given device, creating the fragment if needed.
|
||||
*/
|
||||
def openChat(device: Device.ID): Unit = {
|
||||
currentChat = Some(device)
|
||||
def openChat(address: Address): Unit = {
|
||||
currentChat = Some(address)
|
||||
getFragmentManager
|
||||
.beginTransaction()
|
||||
.detach(ContactsFragment)
|
||||
.add(android.R.id.content, new ChatFragment(device))
|
||||
.add(android.R.id.content, new ChatFragment(address))
|
||||
.commit()
|
||||
getActionBar.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
|
43
app/src/main/scala/com/nutomic/ensichat/aodvv2/Address.scala
Normal file
43
app/src/main/scala/com/nutomic/ensichat/aodvv2/Address.scala
Normal file
|
@ -0,0 +1,43 @@
|
|||
package com.nutomic.ensichat.aodvv2
|
||||
|
||||
import java.util
|
||||
|
||||
import com.google.common.io.BaseEncoding
|
||||
|
||||
object Address {
|
||||
|
||||
val Length = 32
|
||||
|
||||
// 32 bytes, all ones
|
||||
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||
val Broadcast = new Address("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
|
||||
|
||||
// 32 bytes, all zeros
|
||||
// 0x0000000000000000000000000000000000000000000000000000000000000000
|
||||
val Null = new Address("0000000000000000000000000000000000000000000000000000000000000000")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds a device address and provides conversion methods.
|
||||
*
|
||||
* @param Bytes SHA-256 hash of the node's public key.
|
||||
*/
|
||||
class Address(val Bytes: Array[Byte]) {
|
||||
|
||||
require(Bytes.length == Address.Length, "Invalid address length (was " + Bytes.length + ")")
|
||||
|
||||
def this(base16: String) {
|
||||
this(BaseEncoding.base16().decode(base16))
|
||||
}
|
||||
|
||||
override def hashCode = util.Arrays.hashCode(Bytes)
|
||||
|
||||
override def equals(a: Any) = a match {
|
||||
case o: Address => Bytes.deep == o.Bytes.deep
|
||||
case _ => false
|
||||
}
|
||||
|
||||
override def toString = BaseEncoding.base16().encode(Bytes)
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.nutomic.ensichat.aodvv2
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.security.{KeyFactory, PublicKey}
|
||||
|
||||
import com.nutomic.ensichat.messages.Crypto
|
||||
import com.nutomic.ensichat.util.BufferUtils
|
||||
|
||||
object ConnectionInfo {
|
||||
|
||||
val Type = 0
|
||||
|
||||
val HopLimit = 1
|
||||
|
||||
/**
|
||||
* Constructs [[ConnectionInfo]] instance from byte array.
|
||||
*/
|
||||
def read(array: Array[Byte]): ConnectionInfo = {
|
||||
val b = ByteBuffer.wrap(array)
|
||||
val length = BufferUtils.getUnsignedInt(b).toInt
|
||||
val encoded = new Array[Byte](length)
|
||||
b.get(encoded, 0, length)
|
||||
|
||||
val factory = KeyFactory.getInstance(Crypto.KeyAlgorithm)
|
||||
val key = factory.generatePublic(new X509EncodedKeySpec(encoded))
|
||||
new ConnectionInfo(key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds a node's public key.
|
||||
*/
|
||||
class ConnectionInfo(val key: PublicKey) extends MessageBody {
|
||||
|
||||
override def write: Array[Byte] = {
|
||||
val b = ByteBuffer.allocate(4 + key.getEncoded.length)
|
||||
BufferUtils.putUnsignedInt(b, key.getEncoded.length)
|
||||
b.put(key.getEncoded)
|
||||
b.array()
|
||||
}
|
||||
|
||||
}
|
37
app/src/main/scala/com/nutomic/ensichat/aodvv2/Data.scala
Normal file
37
app/src/main/scala/com/nutomic/ensichat/aodvv2/Data.scala
Normal file
|
@ -0,0 +1,37 @@
|
|||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.nutomic.ensichat.aodvv2
|
||||
|
||||
/**
|
||||
* Holds the actual message content.
|
||||
*/
|
||||
abstract class MessageBody {
|
||||
|
||||
/**
|
||||
* Writes the message contents to a byte array.
|
||||
* @return
|
||||
*/
|
||||
def write: Array[Byte]
|
||||
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package com.nutomic.ensichat.aodvv2
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.Date
|
||||
|
||||
import com.nutomic.ensichat.util.BufferUtils
|
||||
|
||||
object MessageHeader {
|
||||
|
||||
val Length = 16 + 2 * Address.Length
|
||||
|
||||
val DefaultHopLimit = 20
|
||||
|
||||
val Version = 3
|
||||
|
||||
class ParseMessageException(detailMessage: String) extends RuntimeException(detailMessage) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs [[MessageHeader]] from byte array.
|
||||
*/
|
||||
def read(bytes: Array[Byte]): MessageHeader = {
|
||||
val b = ByteBuffer.wrap(bytes, 0, Length)
|
||||
|
||||
val versionAndType = BufferUtils.getUnsignedShort(b)
|
||||
val version = versionAndType >>> 12
|
||||
if (version != Version)
|
||||
throw new ParseMessageException("Failed to parse message with unsupported version " + version)
|
||||
val messageType = versionAndType & 0xfff
|
||||
val hopLimit = BufferUtils.getUnsignedByte(b)
|
||||
val hopCount = BufferUtils.getUnsignedByte(b)
|
||||
|
||||
val length = BufferUtils.getUnsignedInt(b)
|
||||
val time = new Date(b.getInt().toLong * 1000)
|
||||
val origin = new Address(BufferUtils.getByteArray(b, Address.Length))
|
||||
val target = new Address(BufferUtils.getByteArray(b, Address.Length))
|
||||
|
||||
val seqNum = BufferUtils.getUnsignedShort(b)
|
||||
val metric = BufferUtils.getUnsignedByte(b)
|
||||
|
||||
new MessageHeader(messageType, hopLimit, time, origin, target, seqNum, metric, length, hopCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* First part of any message, used for routing.
|
||||
*/
|
||||
class MessageHeader(val MessageType: Int,
|
||||
val HopLimit: Int,
|
||||
val Time: Date,
|
||||
val Origin: Address,
|
||||
val Target: Address,
|
||||
val SequenceNumber: Int,
|
||||
val Metric: Int,
|
||||
val Length: Long = -1,
|
||||
val HopCount: Int = 0) {
|
||||
|
||||
/**
|
||||
* Writes the header to byte array.
|
||||
*/
|
||||
def write(body: MessageBody): 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)
|
||||
b.putInt((Time.getTime / 1000).toInt)
|
||||
b.put(Origin.Bytes)
|
||||
b.put(Target.Bytes)
|
||||
|
||||
BufferUtils.putUnsignedShort(b, SequenceNumber)
|
||||
BufferUtils.putUnsignedByte(b, Metric)
|
||||
BufferUtils.putUnsignedByte(b, 0)
|
||||
|
||||
b.array() ++ bodyBytes
|
||||
}
|
||||
|
||||
override def equals(a: Any): Boolean = a match {
|
||||
case o: MessageHeader =>
|
||||
MessageType == o.MessageType &&
|
||||
HopLimit == o.HopLimit &&
|
||||
Time.getTime / 1000 == o.Time.getTime / 1000 &&
|
||||
Origin == o.Origin &&
|
||||
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
|
||||
case _ => false
|
||||
}
|
||||
|
||||
override def toString = "MessageHeader(Version=" + MessageHeader.Version +
|
||||
", Type=" + MessageType + ", HopLimit=" + HopLimit + ", HopCount=" + HopCount +
|
||||
", Time=" + Time + ", Origin=" + Origin + ", Target=" + Target + ", SeqNum=" +
|
||||
", Metric=" + Metric + ", Length=" + Length + ", HopCount=" + HopCount + ")"
|
||||
|
||||
}
|
|
@ -8,10 +8,13 @@ import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
|||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import com.google.common.collect.HashBiMap
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.bluetooth.ChatService.{OnConnectionChangedListener, OnMessageReceivedListener}
|
||||
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}
|
||||
|
@ -26,8 +29,12 @@ object ChatService {
|
|||
|
||||
val KEY_GENERATION_FINISHED = "com.nutomic.ensichat.messages.KEY_GENERATION_FINISHED"
|
||||
|
||||
trait OnConnectionChangedListener {
|
||||
def onConnectionChanged(devices: Map[Device.ID, Device]): Unit
|
||||
/**
|
||||
* Used with [[ChatService.registerConnectionListener]], called when a bluetooth device
|
||||
* connects or disconnects
|
||||
*/
|
||||
trait OnNearbyContactsChangedListener {
|
||||
def onNearbyContactsChanged(devices: Set[Address]): Unit
|
||||
}
|
||||
|
||||
trait OnMessageReceivedListener {
|
||||
|
@ -52,7 +59,7 @@ class ChatService extends Service {
|
|||
* but on a Nexus S (Android 4.1.2), these functions are garbage collected even when
|
||||
* referenced.
|
||||
*/
|
||||
private var connectionListeners = new HashSet[WeakReference[OnConnectionChangedListener]]()
|
||||
private var connectionListeners = new HashSet[WeakReference[OnNearbyContactsChangedListener]]()
|
||||
|
||||
private var messageListeners = Set[WeakReference[OnMessageReceivedListener]]()
|
||||
|
||||
|
@ -68,7 +75,9 @@ class ChatService extends Service {
|
|||
|
||||
private lazy val Database = new Database(this)
|
||||
|
||||
private lazy val Crypto = new Crypto(getFilesDir)
|
||||
private lazy val Crypto = new Crypto(this)
|
||||
|
||||
private val AddressDeviceMap = HashBiMap.create[Address, Device.ID]()
|
||||
|
||||
/**
|
||||
* Initializes BroadcastReceiver for discovery, starts discovery and listens for connections.
|
||||
|
@ -90,7 +99,8 @@ class ChatService extends Service {
|
|||
Crypto.generateLocalKeys()
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
} else
|
||||
Log.i(Tag, "Service started, address is " + Crypto.getLocalAddress)
|
||||
}
|
||||
|
||||
override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
|
||||
|
@ -174,9 +184,9 @@ class ChatService extends Service {
|
|||
/**
|
||||
* Registers a listener that is called whenever a new device is connected.
|
||||
*/
|
||||
def registerConnectionListener(listener: OnConnectionChangedListener): Unit = {
|
||||
connectionListeners += new WeakReference[OnConnectionChangedListener](listener)
|
||||
listener.onConnectionChanged(devices)
|
||||
def registerConnectionListener(listener: OnNearbyContactsChangedListener): Unit = {
|
||||
connectionListeners += new WeakReference[OnNearbyContactsChangedListener](listener)
|
||||
nearbyContactsChanged(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,25 +202,46 @@ class ChatService extends Service {
|
|||
|
||||
if (device.Connected) {
|
||||
connections += (device.Id ->
|
||||
new TransferThread(device, socket, this, Crypto, handleNewMessage))
|
||||
new TransferThread(device, socket, this, Crypto, onReceiveMessage))
|
||||
connections(device.Id).start()
|
||||
connections.apply(device.Id).send(
|
||||
new DeviceInfoMessage(localDeviceId, device.Id, new Date(), Crypto.getLocalPublicKey))
|
||||
} else {
|
||||
Log.i(Tag, device + " has disconnected")
|
||||
AddressDeviceMap.inverse().remove(device.Id)
|
||||
}
|
||||
}
|
||||
|
||||
connectionListeners.foreach(l => l.get match {
|
||||
case Some(x) => x.onConnectionChanged(devices)
|
||||
case None => connectionListeners -= l
|
||||
})
|
||||
/**
|
||||
* Calls listener with [[devices]] (converting [[Device.ID]]s to [[Address]]es.
|
||||
*/
|
||||
def nearbyContactsChanged(listener: OnNearbyContactsChangedListener) = {
|
||||
listener.onNearbyContactsChanged(
|
||||
devices.keySet.map(d => AddressDeviceMap.inverse().get(d)).filter(_ != null))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message to the device specified as receiver,
|
||||
*/
|
||||
def send(message: Message): Unit = {
|
||||
assert(message.sender == localDeviceId, "Message must be sent from local device")
|
||||
connections.apply(message.receiver).send(message)
|
||||
handleNewMessage(message)
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
val header = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit,
|
||||
new Date(), Crypto.getLocalAddress, message.receiver, 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -219,21 +250,64 @@ class ChatService extends Service {
|
|||
* If you want to send a new message, use [[send]].
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
private def handleNewMessage(message: Message): Unit = {
|
||||
assert(message.sender == localDeviceId || message.receiver == localDeviceId,
|
||||
"Message must be sent or received by local device")
|
||||
private def onReceiveMessage(header: MessageHeader, body: MessageBody, device: Device.ID): Unit = {
|
||||
assert(header.Origin != Crypto.getLocalAddress)
|
||||
|
||||
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)
|
||||
}
|
||||
body match {
|
||||
case info: ConnectionInfo =>
|
||||
if (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)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
return
|
||||
}
|
||||
|
||||
if (!Crypto.havePublicKey(sender)) {
|
||||
Crypto.addPublicKey(sender, info.key)
|
||||
Log.i(Tag, "Added public key for new device " + sender.toString)
|
||||
}
|
||||
|
||||
AddressDeviceMap.put(sender, device)
|
||||
Log.i(Tag, "Node " + sender + " connected as " + device)
|
||||
|
||||
connectionListeners.foreach(l => l.get match {
|
||||
case Some(c) => nearbyContactsChanged(c)
|
||||
case None => connectionListeners -= l
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -246,10 +320,8 @@ class ChatService extends Service {
|
|||
/**
|
||||
* Returns the unique bluetooth address of the local device.
|
||||
*/
|
||||
def localDeviceId = new Device.ID(bluetoothAdapter.getAddress)
|
||||
|
||||
def isConnected(device: Device.ID): Boolean = connections.keySet.contains(device)
|
||||
private def localDeviceId = new Device.ID(bluetoothAdapter.getAddress)
|
||||
|
||||
def database = Database
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package com.nutomic.ensichat.bluetooth
|
|||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
|
||||
object Device {
|
||||
private[bluetooth] object Device {
|
||||
|
||||
/**
|
||||
* Holds bluetooth device IDs, which are just wrapped addresses (used for type safety).
|
||||
|
@ -28,7 +28,7 @@ object Device {
|
|||
/**
|
||||
* Holds information about a remote bluetooth device.
|
||||
*/
|
||||
class Device(val Id: Device.ID, val Name: String, val Connected: Boolean,
|
||||
private[bluetooth] class Device(val Id: Device.ID, val Name: String, val Connected: Boolean,
|
||||
btDevice: Option[BluetoothDevice] = None) {
|
||||
|
||||
def this(btDevice: BluetoothDevice, connected: Boolean) {
|
||||
|
@ -37,4 +37,7 @@ class Device(val Id: Device.ID, val Name: String, val Connected: Boolean,
|
|||
|
||||
def bluetoothDevice = btDevice.get
|
||||
|
||||
override def toString = "Device(Id=" + Id + ", Name=" + Name + ", Connected=" + Connected +
|
||||
", btDevice=" + btDevice + ")"
|
||||
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package com.nutomic.ensichat.bluetooth
|
||||
|
||||
import java.io._
|
||||
import java.util.Date
|
||||
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.util.Log
|
||||
import com.nutomic.ensichat.messages.Message._
|
||||
import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message}
|
||||
import org.msgpack.ScalaMessagePack
|
||||
import com.nutomic.ensichat.aodvv2._
|
||||
import com.nutomic.ensichat.messages.Crypto
|
||||
|
||||
/**
|
||||
* Transfers data between connnected devices.
|
||||
|
@ -15,11 +15,11 @@ import org.msgpack.ScalaMessagePack
|
|||
*
|
||||
* @param device The bluetooth device to interact with.
|
||||
* @param socket An open socket to the given device.
|
||||
* @param crypto Object used to handle signing and encryption of messages.
|
||||
* @param onReceive Called when a message was received from the other device.
|
||||
*/
|
||||
class TransferThread(device: Device, socket: BluetoothSocket, service: ChatService,
|
||||
crypto: Crypto, onReceive: (Message) => Unit) extends Thread {
|
||||
crypto: Crypto, onReceive: (MessageHeader, MessageBody, Device.ID) => Unit)
|
||||
extends Thread {
|
||||
|
||||
private val Tag: String = "TransferThread"
|
||||
|
||||
|
@ -44,81 +44,40 @@ 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))
|
||||
|
||||
while (socket.isConnected) {
|
||||
try {
|
||||
val up = new ScalaMessagePack().createUnpacker(InStream)
|
||||
val isEncrypted = up.readBoolean()
|
||||
val plain =
|
||||
if (isEncrypted) {
|
||||
val encrypted = up.readByteArray()
|
||||
val key = up.readByteArray()
|
||||
crypto.decrypt(encrypted, key)
|
||||
} else {
|
||||
up.readByteArray()
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
val (message, signature) = Message.read(plain)
|
||||
var messageValid = true
|
||||
|
||||
if (message.sender != device.Id) {
|
||||
Log.i(Tag, "Dropping message with invalid sender from " + device.Id)
|
||||
messageValid = false
|
||||
}
|
||||
|
||||
if (message.receiver != service.localDeviceId) {
|
||||
Log.i(Tag, "Dropping message with different receiver from " + device.Id)
|
||||
messageValid = false
|
||||
}
|
||||
|
||||
// Add public key for new, directly connected device.
|
||||
// Explicitly check that message was not forwarded or spoofed.
|
||||
if (message.isInstanceOf[DeviceInfoMessage] && !crypto.havePublicKey(message.sender) &&
|
||||
message.sender == device.Id) {
|
||||
val dim = message.asInstanceOf[DeviceInfoMessage]
|
||||
// Permanently store public key for new local devices (also check signature).
|
||||
if (crypto.isValidSignature(message, signature, dim.publicKey)) {
|
||||
crypto.addPublicKey(device.Id, dim.publicKey)
|
||||
Log.i(Tag, "Added public key for new device " + device.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if (!crypto.isValidSignature(message, signature)) {
|
||||
Log.i(Tag, "Dropping message with invalid signature from " + device.Id)
|
||||
messageValid = false
|
||||
}
|
||||
|
||||
if (messageValid) {
|
||||
message match {
|
||||
case m: DeviceInfoMessage => crypto.addPublicKey(message.sender, m.publicKey)
|
||||
case _ => onReceive(message)
|
||||
}
|
||||
}
|
||||
onReceive(header, body, device.Id)
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
Log.i(Tag, "Failed to read incoming message", e)
|
||||
case e: RuntimeException =>
|
||||
Log.i(Tag, "Received invalid message", e)
|
||||
case e: IOException =>
|
||||
Log.w(Tag, "Failed to read incoming message", e)
|
||||
return
|
||||
}
|
||||
}
|
||||
service.onConnectionChanged(new Device(device.bluetoothDevice, false), null)
|
||||
}
|
||||
|
||||
def send(message: Message): Unit = {
|
||||
def send(header: MessageHeader, body: MessageBody): Unit = {
|
||||
try {
|
||||
val plain = message.write(crypto.calculateSignature(message))
|
||||
val packer = new ScalaMessagePack().createPacker(OutStream)
|
||||
|
||||
message.messageType match {
|
||||
case Type.Text =>
|
||||
val (encrypted, key) = crypto.encrypt(message.receiver, plain)
|
||||
// Message is encrypted.
|
||||
packer.write(true)
|
||||
.write(encrypted)
|
||||
.write(key)
|
||||
case Type.DeviceInfo | Type.RequestAddContact | Type.ResultAddContact =>
|
||||
// Message is not encrypted.
|
||||
packer.write(false)
|
||||
.write(plain)
|
||||
}
|
||||
OutStream.write(header.write(body))
|
||||
} catch {
|
||||
case e: IOException => Log.e(Tag, "Failed to write message", e)
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ 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.bluetooth.ChatService
|
||||
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
|
||||
import com.nutomic.ensichat.bluetooth.{ChatService, Device}
|
||||
import com.nutomic.ensichat.messages.{Message, TextMessage}
|
||||
import com.nutomic.ensichat.messages.{Crypto, Message, TextMessage}
|
||||
import com.nutomic.ensichat.util.MessagesAdapter
|
||||
|
||||
import scala.collection.SortedSet
|
||||
|
@ -24,12 +25,15 @@ import scala.collection.SortedSet
|
|||
class ChatFragment extends ListFragment with OnClickListener
|
||||
with OnMessageReceivedListener {
|
||||
|
||||
def this(device: Device.ID) {
|
||||
/**
|
||||
* Fragments need to have a default constructor, so this is optional.
|
||||
*/
|
||||
def this(address: Address) {
|
||||
this
|
||||
this.device = device
|
||||
this.address = address
|
||||
}
|
||||
|
||||
private var device: Device.ID = _
|
||||
private var address: Address = _
|
||||
|
||||
private var chatService: ChatService = _
|
||||
|
||||
|
@ -49,9 +53,9 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
chatService = activity.service
|
||||
|
||||
// Read local device ID from service,
|
||||
adapter = new MessagesAdapter(getActivity, chatService.localDeviceId)
|
||||
adapter = new MessagesAdapter(getActivity, address)
|
||||
chatService.registerMessageListener(ChatFragment.this)
|
||||
onMessageReceived(chatService.database.getMessages(device, 15))
|
||||
onMessageReceived(chatService.database.getMessages(address, 15))
|
||||
|
||||
if (listView != null) {
|
||||
listView.setAdapter(adapter)
|
||||
|
@ -83,13 +87,13 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
device = new Device.ID(savedInstanceState.getString("device"))
|
||||
address = new Address(savedInstanceState.getByteArray("device"))
|
||||
}
|
||||
}
|
||||
|
||||
override def onSaveInstanceState(outState: Bundle): Unit = {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("device", device.toString)
|
||||
outState.putByteArray("device", address.Bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,14 +101,12 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
*/
|
||||
override def onClick(view: View): Unit = view.getId match {
|
||||
case R.id.send =>
|
||||
val text: String = messageText.getText.toString.trim
|
||||
val text = messageText.getText.toString.trim
|
||||
if (!text.isEmpty) {
|
||||
if (!chatService.isConnected(device)) {
|
||||
Toast.makeText(getActivity, R.string.contact_offline_toast, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
chatService.send(
|
||||
new TextMessage(chatService.localDeviceId, device, new Date(), text.toString))
|
||||
val message =
|
||||
new TextMessage(Crypto.getLocalAddress(getActivity), address, new Date(), text.toString)
|
||||
chatService.send(message)
|
||||
adapter.add(message)
|
||||
messageText.getText.clear()
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +115,7 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
* Displays new messages in UI.
|
||||
*/
|
||||
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
|
||||
messages.filter(m => m.sender == device || m.receiver == device)
|
||||
messages.filter(m => m.sender == address || m.receiver == address)
|
||||
.filter(_.isInstanceOf[TextMessage])
|
||||
.foreach(m => adapter.add(m.asInstanceOf[TextMessage]))
|
||||
}
|
||||
|
@ -121,6 +123,6 @@ class ChatFragment extends ListFragment with OnClickListener
|
|||
/**
|
||||
* Returns the device that this fragment shows chats for.
|
||||
*/
|
||||
def getDevice = this.device
|
||||
def getDevice = address
|
||||
|
||||
}
|
||||
|
|
|
@ -64,6 +64,6 @@ class ContactsFragment extends ListFragment {
|
|||
* Opens a chat with the clicked device.
|
||||
*/
|
||||
override def onListItemClick(l: ListView, v: View, position: Int, id: Long): Unit =
|
||||
getActivity.asInstanceOf[MainActivity].openChat(Adapter.getItem(position).Id)
|
||||
getActivity.asInstanceOf[MainActivity].openChat(Adapter.getItem(position))
|
||||
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
|
|||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.aodvv2.Address
|
||||
import com.nutomic.ensichat.messages.Crypto._
|
||||
import com.nutomic.ensichat.util.PRNGFixes
|
||||
|
||||
|
@ -38,16 +40,28 @@ object Crypto {
|
|||
*/
|
||||
val SymmetricKeyAlgorithm = "AES/CBC/PKCS5Padding"
|
||||
|
||||
/**
|
||||
* Algorithm used to hash PublicKey and get the address.
|
||||
*/
|
||||
val KeyHashAlgorithm = "SHA-256"
|
||||
|
||||
private val LocalAddressKey = "local_address"
|
||||
|
||||
/**
|
||||
* Returns the address of the local node.
|
||||
*/
|
||||
def getLocalAddress(context: Context) = new Address(
|
||||
PreferenceManager.getDefaultSharedPreferences(context).getString(LocalAddressKey, null))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all cryptography related operations.
|
||||
*
|
||||
* @param filesDir The return value of [[android.content.Context#getFilesDir]].
|
||||
* @note We can't use [[KeyStore]], because it requires certificates, and does not work for
|
||||
* private keys
|
||||
*/
|
||||
class Crypto(filesDir: File) {
|
||||
class Crypto(Context: Context) {
|
||||
|
||||
private val Tag = "Crypto"
|
||||
|
||||
|
@ -61,27 +75,40 @@ class Crypto(filesDir: File) {
|
|||
* Generates a new key pair using [[KeyAlgorithm]] with [[KeySize]] bits and stores the keys.
|
||||
*/
|
||||
def generateLocalKeys(): Unit = {
|
||||
Log.i(Tag, "Generating cryptographic keys with algorithm: " + KeyAlgorithm)
|
||||
val keyGen = KeyPairGenerator.getInstance(KeyAlgorithm)
|
||||
keyGen.initialize(KeySize)
|
||||
val keyPair = keyGen.genKeyPair()
|
||||
var address: Address = null
|
||||
var keyPair: KeyPair = null
|
||||
do {
|
||||
val keyGen = KeyPairGenerator.getInstance(KeyAlgorithm)
|
||||
keyGen.initialize(KeySize)
|
||||
keyPair = keyGen.genKeyPair()
|
||||
|
||||
address = calculateAddress(keyPair.getPublic)
|
||||
|
||||
// The hash must have at least one bit set to not collide with the broadcast address.
|
||||
} while(address == Address.Broadcast || address == Address.Null)
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(Context)
|
||||
.edit()
|
||||
.putString(Crypto.LocalAddressKey, address.toString)
|
||||
.commit()
|
||||
|
||||
saveKey(PrivateKeyAlias, keyPair.getPrivate)
|
||||
saveKey(PublicKeyAlias, keyPair.getPublic)
|
||||
Log.i(Tag, "Generating cryptographic keys, address is " + address)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we have a public key stored for the given device.
|
||||
*/
|
||||
def havePublicKey(device: Device.ID): Boolean = new File(keyFolder, device.toString).exists()
|
||||
def havePublicKey(address: Address): Boolean = new File(keyFolder, address.toString).exists()
|
||||
|
||||
/**
|
||||
* Returns the public key for the given device.
|
||||
*
|
||||
* @throws RuntimeException If the key does not exist.
|
||||
*/
|
||||
def getPublicKey(device: Device.ID): PublicKey = {
|
||||
loadKey(device.toString, classOf[PublicKey])
|
||||
def getPublicKey(address: Address): PublicKey = {
|
||||
loadKey(address.toString, classOf[PublicKey])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,14 +116,14 @@ class Crypto(filesDir: File) {
|
|||
*
|
||||
* If a key for the device already exists, nothing is done.
|
||||
*
|
||||
* @param device The device to wchi the key belongs.
|
||||
* @param address The device to which the key belongs.
|
||||
* @param key The new key to add.
|
||||
*/
|
||||
def addPublicKey(device: Device.ID, key: PublicKey): Unit = {
|
||||
if (!havePublicKey(device)) {
|
||||
saveKey(device.toString, key)
|
||||
def addPublicKey(address: Address, key: PublicKey): Unit = {
|
||||
if (!havePublicKey(address)) {
|
||||
saveKey(address.toString, key)
|
||||
} else {
|
||||
Log.i(Tag, "Already have key for " + device.toString + ", not overwriting")
|
||||
Log.i(Tag, "Already have key for " + address.toString + ", not overwriting")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,7 +237,7 @@ class Crypto(filesDir: File) {
|
|||
/**
|
||||
* Returns the folder where keys are stored.
|
||||
*/
|
||||
private def keyFolder = new File(filesDir, "keys")
|
||||
private def keyFolder = new File(Context.getFilesDir, "keys")
|
||||
|
||||
/**
|
||||
* Encrypts data for the given receiver.
|
||||
|
@ -220,7 +247,7 @@ class Crypto(filesDir: File) {
|
|||
* @param key Optional RSA public key to use for encryption.
|
||||
* @return Pair of AES encrypted data and RSA encrypted AES key.
|
||||
*/
|
||||
def encrypt(receiver: Device.ID, data: Array[Byte], key: PublicKey = null):
|
||||
def encrypt(receiver: Address, data: Array[Byte], key: PublicKey = null):
|
||||
(Array[Byte], Array[Byte]) = {
|
||||
// Symmetric encryption of data
|
||||
val secretKey = makeSecretKey()
|
||||
|
@ -290,4 +317,15 @@ class Crypto(filesDir: File) {
|
|||
new SecretKeySpec(key.getEncoded, SymmetricKeyAlgorithm)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the given public key and returns the hash as address.
|
||||
*/
|
||||
def calculateAddress(key: PublicKey): Address = {
|
||||
val md = MessageDigest.getInstance(KeyHashAlgorithm)
|
||||
val hash = md.digest(key.getEncoded)
|
||||
new Address(hash)
|
||||
}
|
||||
|
||||
def getLocalAddress = Crypto.getLocalAddress(Context)
|
||||
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
package com.nutomic.ensichat.messages
|
||||
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.security.{KeyFactory, PublicKey}
|
||||
import java.util.{Date, Objects}
|
||||
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.messages.Message._
|
||||
import org.msgpack.packer.Packer
|
||||
import org.msgpack.unpacker.Unpacker
|
||||
|
||||
object DeviceInfoMessage {
|
||||
|
||||
private val FieldPublicKey = "public-key"
|
||||
|
||||
def read(sender: Device.ID, receiver: Device.ID, date: Date, up: Unpacker): DeviceInfoMessage = {
|
||||
val factory = KeyFactory.getInstance(Crypto.KeyAlgorithm)
|
||||
val key = factory.generatePublic(new X509EncodedKeySpec(up.readByteArray()))
|
||||
new DeviceInfoMessage(sender, receiver, date, key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Message that contains the public key of a device.
|
||||
*
|
||||
* Used on first connection to a new (local) device for key exchange.
|
||||
*/
|
||||
class DeviceInfoMessage(override val sender: Device.ID, override val receiver: Device.ID,
|
||||
override val date: Date, val publicKey: PublicKey)
|
||||
extends Message(Type.DeviceInfo) {
|
||||
|
||||
override def doWrite(packer: Packer) = packer.write(publicKey.getEncoded)
|
||||
|
||||
override def equals(a: Any) =
|
||||
super.equals(a) && a.asInstanceOf[DeviceInfoMessage].publicKey.toString == publicKey.toString
|
||||
|
||||
override def hashCode = Objects.hash(super.hashCode: java.lang.Integer, publicKey)
|
||||
|
||||
override def toString = "DeviceInfoMessage(" + sender.toString + ", " + receiver.toString +
|
||||
", " + date.toString + ", " + publicKey.toString + ")"
|
||||
|
||||
}
|
|
@ -3,7 +3,7 @@ package com.nutomic.ensichat.messages
|
|||
import java.io.IOException
|
||||
import java.util.{Date, Objects}
|
||||
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.aodvv2.Address
|
||||
import org.msgpack.ScalaMessagePack
|
||||
import org.msgpack.packer.Packer
|
||||
|
||||
|
@ -16,9 +16,8 @@ object Message {
|
|||
*/
|
||||
object Type {
|
||||
val Text = 1
|
||||
val DeviceInfo = 2
|
||||
val RequestAddContact = 3
|
||||
val ResultAddContact = 4
|
||||
val RequestAddContact = 2
|
||||
val ResultAddContact = 3
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,13 +39,12 @@ object Message {
|
|||
@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 Device.ID(up.readString())
|
||||
val receiver = new Device.ID(up.readString())
|
||||
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.DeviceInfo => DeviceInfoMessage.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 =>
|
||||
|
@ -61,17 +59,18 @@ object Message {
|
|||
*
|
||||
* @param messageType One of [[Message.Type]].
|
||||
*/
|
||||
@Deprecated
|
||||
abstract class Message(val messageType: Int) {
|
||||
|
||||
/**
|
||||
* Device where the message was sent from.
|
||||
*/
|
||||
val sender: Device.ID
|
||||
val sender: Address
|
||||
|
||||
/**
|
||||
* Device the message is addressed to.
|
||||
*/
|
||||
val receiver: Device.ID
|
||||
val receiver: Address
|
||||
|
||||
/**
|
||||
* Timestamp of message creation.
|
||||
|
@ -86,8 +85,8 @@ abstract class Message(val messageType: Int) {
|
|||
def write(signature: Array[Byte]): Array[Byte] = {
|
||||
val packer = new ScalaMessagePack().createBufferPacker()
|
||||
packer.write(messageType)
|
||||
.write(sender.toString)
|
||||
.write(receiver.toString)
|
||||
.write(sender.Bytes)
|
||||
.write(receiver.Bytes)
|
||||
.write(date.getTime)
|
||||
.write(signature)
|
||||
doWrite(packer)
|
||||
|
|
|
@ -3,14 +3,14 @@ package com.nutomic.ensichat.messages
|
|||
import java.util.Date
|
||||
|
||||
import com.nutomic.ensichat.activities.AddContactsActivity
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
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: Device.ID, receiver: Device.ID, date: Date, up: Unpacker) =
|
||||
def read(sender: Address, receiver: Address, date: Date, up: Unpacker) =
|
||||
new RequestAddContactMessage(sender, receiver, date)
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ object RequestAddContactMessage {
|
|||
/**
|
||||
* Message sent by [[AddContactsActivity]] to notify a device that it should be added as a contact.
|
||||
*/
|
||||
class RequestAddContactMessage(override val sender: Device.ID, override val receiver: Device.ID,
|
||||
class RequestAddContactMessage(override val sender: Address, override val receiver: Address,
|
||||
override val date: Date) extends Message(Type.RequestAddContact) {
|
||||
|
||||
override def doWrite(packer: Packer) = {
|
||||
|
|
|
@ -3,14 +3,14 @@ package com.nutomic.ensichat.messages
|
|||
import java.util.{Date, Objects}
|
||||
|
||||
import com.nutomic.ensichat.activities.AddContactsActivity
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
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: Device.ID, receiver: Device.ID, date: Date, up: Unpacker) =
|
||||
def read(sender: Address, receiver: Address, date: Date, up: Unpacker) =
|
||||
new ResultAddContactMessage(sender, receiver, date, up.readBoolean())
|
||||
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ object ResultAddContactMessage {
|
|||
* Message sent by [[AddContactsActivity]] to tell a device whether the user confirmed adding it
|
||||
* to contacts.
|
||||
*/
|
||||
class ResultAddContactMessage(override val sender: Device.ID, override val receiver: Device.ID,
|
||||
class ResultAddContactMessage(override val sender: Address, override val receiver: Address,
|
||||
override val date: Date, val Accepted: Boolean)
|
||||
extends Message(Type.ResultAddContact) {
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ package com.nutomic.ensichat.messages
|
|||
|
||||
import java.util.{Date, Objects}
|
||||
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
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: Device.ID, receiver: Device.ID, date: Date, up: Unpacker): TextMessage =
|
||||
def read(sender: Address, receiver: Address, date: Date, up: Unpacker): TextMessage =
|
||||
new TextMessage(sender, receiver, date, up.readString())
|
||||
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ object TextMessage {
|
|||
/**
|
||||
* Message that contains text.
|
||||
*/
|
||||
class TextMessage(override val sender: Device.ID, override val receiver: Device.ID,
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object BufferUtils {
|
||||
|
||||
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
|
||||
|
||||
def putUnsignedByte(bb: ByteBuffer, value: Int) = bb.put((value & 0xff).toByte)
|
||||
|
||||
def getUnsignedShort(bb: ByteBuffer): Int = bb.getShort & 0xffff
|
||||
|
||||
def putUnsignedShort(bb: ByteBuffer, value: Int) = bb.putShort((value & 0xffff).toShort)
|
||||
|
||||
def getUnsignedInt(bb: ByteBuffer): Long = bb.getInt.toLong & 0xffffffffL
|
||||
|
||||
def putUnsignedInt(bb: ByteBuffer, value: Long) = bb.putInt((value & 0xffffffffL).toInt)
|
||||
|
||||
/**
|
||||
* Reads a byte array with the given length and returns it.
|
||||
*/
|
||||
def getByteArray(bb: ByteBuffer, numBytes: Int): Array[Byte] = {
|
||||
val b = new Array[Byte](numBytes)
|
||||
bb.get(b, 0, numBytes)
|
||||
b
|
||||
}
|
||||
|
||||
def toString(bb: ByteBuffer)= bb.array().slice(0, 4).map("%02X" format _).mkString
|
||||
|
||||
}
|
|
@ -4,7 +4,7 @@ import java.util.Date
|
|||
|
||||
import android.content.{ContentValues, Context}
|
||||
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
import com.nutomic.ensichat.aodvv2.Address
|
||||
import com.nutomic.ensichat.messages._
|
||||
|
||||
import scala.collection.SortedSet
|
||||
|
@ -18,15 +18,14 @@ object Database {
|
|||
|
||||
private val CreateMessagesTable = "CREATE TABLE messages(" +
|
||||
"_id integer primary key autoincrement," +
|
||||
"sender string not null," +
|
||||
"receiver string not null," +
|
||||
"text blob not null," +
|
||||
"sender text not null," +
|
||||
"receiver text not null," +
|
||||
"text text not null," +
|
||||
"date integer not null);" // Unix timestamp of message.
|
||||
|
||||
private val CreateContactsTable = "CREATE TABLE contacts(" +
|
||||
"_id integer primary key autoincrement," +
|
||||
"device_id string not null," +
|
||||
"name string not null)"
|
||||
"address text not null)"
|
||||
|
||||
}
|
||||
|
||||
|
@ -48,16 +47,16 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
|
|||
/**
|
||||
* Returns the count last messages for device.
|
||||
*/
|
||||
def getMessages(device: Device.ID, count: Int): SortedSet[Message] = {
|
||||
def getMessages(address: Address, count: Int): SortedSet[Message] = {
|
||||
val c = getReadableDatabase.query(true,
|
||||
"messages", Array("sender", "receiver", "text", "date"),
|
||||
"sender = ? OR receiver = ?", Array(device.toString, device.toString),
|
||||
"sender = ? OR receiver = ?", 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 Device.ID(c.getString(c.getColumnIndex("sender"))),
|
||||
new Device.ID(c.getString(c.getColumnIndex("receiver"))),
|
||||
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
|
||||
|
@ -78,20 +77,19 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
|
|||
cv.put("date", msg.date.getTime.toString)
|
||||
cv.put("text", msg.text)
|
||||
getWritableDatabase.insert("messages", null, cv)
|
||||
case _: DeviceInfoMessage | _: RequestAddContactMessage | _: ResultAddContactMessage =>
|
||||
case _: RequestAddContactMessage | _: ResultAddContactMessage =>
|
||||
// Never stored.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all contacts of this device.
|
||||
*/
|
||||
def getContacts: Set[Device] = {
|
||||
val c = getReadableDatabase.query(true, "contacts", Array("device_id", "name"), "", Array(),
|
||||
null, null, "name DESC", null)
|
||||
var contacts = Set[Device]()
|
||||
def getContacts: Set[Address] = {
|
||||
val c = getReadableDatabase.query(true, "contacts", Array("address"), "", Array(),
|
||||
null, null, null, null)
|
||||
var contacts = Set[Address]()
|
||||
while (c.moveToNext()) {
|
||||
contacts += new Device(new Device.ID(c.getString(c.getColumnIndex("device_id"))),
|
||||
c.getString(c.getColumnIndex("name")), false)
|
||||
contacts += new Address(c.getString(c.getColumnIndex("address")))
|
||||
}
|
||||
c.close()
|
||||
contacts
|
||||
|
@ -100,19 +98,18 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
|
|||
/**
|
||||
* Returns true if a contact with the given device ID exists.
|
||||
*/
|
||||
def isContact(device: Device.ID): Boolean = {
|
||||
val c = getReadableDatabase.query(true, "contacts", Array("_id"), "device_id = ?",
|
||||
Array(device.toString), null, null, null, null)
|
||||
def isContact(address: Address): Boolean = {
|
||||
val c = getReadableDatabase.query(true, "contacts", Array("_id"), "address = ?",
|
||||
Array(address.toString), null, null, null, null)
|
||||
c.getCount != 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given device into contacts.
|
||||
*/
|
||||
def addContact(device: Device): Unit = {
|
||||
def addContact(address: Address): Unit = {
|
||||
val cv = new ContentValues()
|
||||
cv.put("device_id", device.Id.toString)
|
||||
cv.put("name", device.Name)
|
||||
cv.put("address", address.toString)
|
||||
getWritableDatabase.insert("contacts", null, cv)
|
||||
contactsUpdatedListeners.foreach(_())
|
||||
}
|
||||
|
|
|
@ -3,18 +3,19 @@ package com.nutomic.ensichat.util
|
|||
import android.content.Context
|
||||
import android.view.{View, ViewGroup}
|
||||
import android.widget.{ArrayAdapter, TextView}
|
||||
import com.nutomic.ensichat.aodvv2.Address
|
||||
import com.nutomic.ensichat.bluetooth.Device
|
||||
|
||||
/**
|
||||
* Displays [[Device]]s in ListView.
|
||||
*/
|
||||
class DevicesAdapter(context: Context) extends
|
||||
ArrayAdapter[Device](context, android.R.layout.simple_list_item_1) {
|
||||
ArrayAdapter[Address](context, android.R.layout.simple_list_item_1) {
|
||||
|
||||
override def getView(position: Int, convertView: View, parent: ViewGroup): View = {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val title: TextView = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
|
||||
title.setText(getItem(position).Name)
|
||||
title.setText(getItem(position).toString)
|
||||
view
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import java.security.{MessageDigest, PublicKey}
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap.Config
|
||||
import android.graphics.{Bitmap, Canvas, Color}
|
||||
import com.nutomic.ensichat.aodvv2.Address
|
||||
|
||||
/**
|
||||
* Calculates a unique identicon for the given hash.
|
||||
|
@ -25,10 +24,8 @@ object IdenticonGenerator {
|
|||
*
|
||||
* @param size The size of the bitmap returned.
|
||||
*/
|
||||
def generate(key: PublicKey, size: (Int, Int), context: Context): Bitmap = {
|
||||
// Hash the key.
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
val hash = digest.digest(key.getEncoded)
|
||||
def generate(address: Address, size: (Int, Int), context: Context): Bitmap = {
|
||||
val hash = address.Bytes
|
||||
|
||||
// Create base image and colors.
|
||||
var identicon = Bitmap.createBitmap(Width, Height, Config.ARGB_8888)
|
||||
|
|
|
@ -4,13 +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.bluetooth.Device
|
||||
import com.nutomic.ensichat.aodvv2.Address
|
||||
import com.nutomic.ensichat.messages.TextMessage
|
||||
|
||||
/**
|
||||
* Displays [[TextMessage]]s in ListView.
|
||||
*/
|
||||
class MessagesAdapter(context: Context, localDevice: Device.ID) extends
|
||||
class MessagesAdapter(context: Context, remoteAddress: Address) extends
|
||||
ArrayAdapter[TextMessage](context, R.layout.item_message, android.R.id.text1) {
|
||||
|
||||
/**
|
||||
|
@ -26,7 +26,7 @@ class MessagesAdapter(context: Context, localDevice: Device.ID) extends
|
|||
|
||||
val lp = new RelativeLayout.LayoutParams(tv.getLayoutParams)
|
||||
val margin = (MessageMargin * context.getResources.getDisplayMetrics.density).toInt
|
||||
if (getItem(position).sender == localDevice) {
|
||||
if (getItem(position).sender != remoteAddress) {
|
||||
view.setGravity(Gravity.RIGHT)
|
||||
lp.setMargins(margin, 0, 0, 0)
|
||||
} else {
|
||||
|
|
Reference in a new issue