Added message signing.

Also, DeviceInfoMessage is now sent on connect for key exchange,
new abstract class Message is superclass of all message
implementations.

Multiple minor reformats/refactorings.
This commit is contained in:
Felix Ableitner 2014-11-06 13:25:47 +02:00
parent bf7aab1e11
commit 4cbaf975b5
15 changed files with 561 additions and 143 deletions

View file

@ -0,0 +1,24 @@
package com.nutomic.ensichat.messages
import android.test.AndroidTestCase
import junit.framework.Assert._
import com.nutomic.ensichat.messages.MessageTest._
class CryptoTest extends AndroidTestCase {
var encrypt: Crypto = _
override def setUp(): Unit = {
super.setUp()
encrypt = new Crypto(getContext.getFilesDir)
if (!encrypt.localKeysExist) {
encrypt.generateLocalKeys()
}
}
def testSignVerify(): Unit = {
val sig = encrypt.calculateSignature(m1)
assertTrue(encrypt.isValidSignature(m1, sig, encrypt.getLocalPublicKey))
}
}

View file

@ -9,6 +9,7 @@ import android.test.AndroidTestCase
import android.test.mock.MockContext
import com.nutomic.ensichat.bluetooth.Device
import junit.framework.Assert._
import com.nutomic.ensichat.messages.MessageTest._
class MessageStoreTest extends AndroidTestCase {
@ -26,9 +27,9 @@ class MessageStoreTest extends AndroidTestCase {
override def setUp(): Unit = {
MessageStore = new MessageStore(new TestContext(getContext))
MessageStore.addMessage(TextMessageTest.m1)
MessageStore.addMessage(TextMessageTest.m2)
MessageStore.addMessage(TextMessageTest.m3)
MessageStore.addMessage(m1)
MessageStore.addMessage(m2)
MessageStore.addMessage(m3)
}
override def tearDown(): Unit = {
@ -46,13 +47,13 @@ class MessageStoreTest extends AndroidTestCase {
def testOrder(): Unit = {
val msg = MessageStore.getMessages(new Device.ID("two"), 1)
assertTrue(msg.contains(TextMessageTest.m3))
assertTrue(msg.contains(m3))
}
def testSelect(): Unit = {
val msg = MessageStore.getMessages(new Device.ID("two"), 2)
assertTrue(msg.contains(TextMessageTest.m1))
assertTrue(msg.contains(TextMessageTest.m3))
assertTrue(msg.contains(m1))
assertTrue(msg.contains(m3))
}
}

View file

@ -0,0 +1,49 @@
package com.nutomic.ensichat.messages
import java.io.{PipedInputStream, PipedOutputStream}
import java.util.GregorianCalendar
import android.test.AndroidTestCase
import com.nutomic.ensichat.bluetooth.Device
import com.nutomic.ensichat.messages.MessageTest._
import junit.framework.Assert._
import scala.collection.immutable.TreeSet
object MessageTest {
val m1 = new TextMessage(new Device.ID("one"), new Device.ID("two"),
new GregorianCalendar(2014, 10, 29).getTime, "first")
val m2 = new TextMessage(new Device.ID("one"), new Device.ID("three"),
new GregorianCalendar(2014, 10, 30).getTime, "second")
val m3 = new TextMessage(new Device.ID("four"), new Device.ID("two"),
new GregorianCalendar(2014, 10, 31).getTime, "third")
}
class MessageTest extends AndroidTestCase {
def testSerialize(): Unit = {
val pis = new PipedInputStream()
val pos = new PipedOutputStream(pis)
m1.write(pos, Array[Byte]())
val (msg, _) = Message.read(pis)
assertEquals(m1, msg)
}
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)
}
}

View file

@ -1,51 +0,0 @@
package com.nutomic.ensichat.messages
import java.io.{PipedInputStream, PipedOutputStream}
import java.util.GregorianCalendar
import android.test.AndroidTestCase
import com.nutomic.ensichat.bluetooth.Device
import junit.framework.Assert._
import scala.collection.immutable.TreeSet
object TextMessageTest {
val m1 = new TextMessage(new Device.ID("one"), new Device.ID("two"), "first",
new GregorianCalendar(2014, 10, 29).getTime)
val m2 = new TextMessage(new Device.ID("one"), new Device.ID("three"), "second",
new GregorianCalendar(2014, 10, 30).getTime)
val m3 = new TextMessage(new Device.ID("four"), new Device.ID("two"), "third",
new GregorianCalendar(2014, 10, 31).getTime)
}
class TextMessageTest extends AndroidTestCase {
def testSerialize(): Unit = {
val pis = new PipedInputStream()
val pos = new PipedOutputStream(pis)
TextMessageTest.m1.write(pos)
val unpacked = TextMessage.fromStream(pis)
assertEquals(TextMessageTest.m1, unpacked)
}
def testOrder(): Unit = {
var messages = new TreeSet[TextMessage]()(TextMessage.Ordering)
messages += TextMessageTest.m1
messages += TextMessageTest.m2
assertEquals(TextMessageTest.m1, messages.firstKey)
messages = new TreeSet[TextMessage]()(TextMessage.Ordering)
messages += TextMessageTest.m2
messages += TextMessageTest.m3
assertEquals(TextMessageTest.m2, messages.firstKey)
}
}

View file

@ -1,6 +1,6 @@
package com.nutomic.ensichat.bluetooth
import java.util.UUID
import java.util.{Date, UUID}
import android.app.Service
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
@ -9,7 +9,7 @@ import android.os.Handler
import android.util.Log
import com.nutomic.ensichat.R
import com.nutomic.ensichat.bluetooth.ChatService.{OnDeviceConnectedListener, OnMessageReceivedListener}
import com.nutomic.ensichat.messages.{MessageStore, TextMessage}
import com.nutomic.ensichat.messages._
import scala.collection.immutable.{HashMap, HashSet, TreeSet}
import scala.collection.{SortedSet, mutable}
@ -22,12 +22,14 @@ object ChatService {
*/
val appUuid: UUID = UUID.fromString("8ed52b7a-4501-5348-b054-3d94d004656e")
val KEY_GENERATION_FINISHED = "com.nutomic.ensichat.messages.KEY_GENERATION_FINISHED"
trait OnDeviceConnectedListener {
def onDeviceConnected(devices: Map[Device.ID, Device]): Unit
}
trait OnMessageReceivedListener {
def onMessageReceived(messages: SortedSet[TextMessage]): Unit
def onMessageReceived(messages: SortedSet[Message]): Unit
}
}
@ -68,6 +70,8 @@ class ChatService extends Service {
private var MessageStore: MessageStore = _
private lazy val Encrypt = new Crypto(getFilesDir)
/**
* Initializes BroadcastReceiver for discovery, starts discovery and listens for connections.
*/
@ -83,6 +87,14 @@ class ChatService extends Service {
if (bluetoothAdapter.isEnabled) {
startBluetoothConnections()
}
if (!Encrypt.localKeysExist) {
new Thread(new Runnable {
override def run(): Unit = {
Encrypt.generateLocalKeys()
}
}).start()
}
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
@ -120,7 +132,7 @@ class ChatService extends Service {
/**
* Receives newly discovered devices and connects to them.
*/
private val DeviceDiscoveredReceiver: BroadcastReceiver = new BroadcastReceiver() {
private val DeviceDiscoveredReceiver = new BroadcastReceiver() {
override def onReceive(context: Context, intent: Intent) {
val device: Device =
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
@ -132,7 +144,7 @@ class ChatService extends Service {
/**
* Starts or stops listening and discovery based on bluetooth state.
*/
private val BluetoothStateReceiver: BroadcastReceiver = new BroadcastReceiver {
private val BluetoothStateReceiver = new BroadcastReceiver {
override def onReceive(context: Context, intent: Intent): Unit = {
intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) match {
case BluetoothAdapter.STATE_ON =>
@ -171,13 +183,16 @@ class ChatService extends Service {
/**
* Called when a Bluetooth device is connected.
*
* Adds the device to [[connections]], notifies all [[deviceListeners]].
* Adds the device to [[connections]], notifies all [[deviceListeners]], sends DeviceInfoMessage.
*/
def onConnected(device: Device, socket: BluetoothSocket): Unit = {
val updatedDevice: Device = new Device(device.bluetoothDevice, true)
devices += (device.id -> updatedDevice)
connections += (device.id -> new TransferThread(updatedDevice, socket, handleNewMessage))
connections += (device.id ->
new TransferThread(updatedDevice, socket, localDeviceId, Encrypt, handleNewMessage))
connections(device.id).start()
send(new DeviceInfoMessage(localDeviceId, device.id, new Date(), Encrypt.getLocalPublicKey))
deviceListeners.foreach(l => l.get match {
case Some(_) => l.apply().onDeviceConnected(devices)
case None => deviceListeners -= l
@ -187,7 +202,7 @@ class ChatService extends Service {
/**
* Sends message to the device specified as receiver,
*/
def send(message: TextMessage): Unit = {
def send(message: Message): Unit = {
connections.apply(message.receiver).send(message)
handleNewMessage(message)
}
@ -197,13 +212,13 @@ class ChatService extends Service {
*
* If you want to send a new message, use [[send]].
*/
def handleNewMessage(message: TextMessage): Unit = {
private def handleNewMessage(message: Message): Unit = {
MessageStore.addMessage(message)
MainHandler.post(new Runnable {
override def run(): Unit = {
messageListeners(message.sender).foreach(l =>
if (l.get != null)
l.apply().onMessageReceived(new TreeSet[TextMessage]()(TextMessage.Ordering) + message)
l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message)
else
messageListeners(message.sender) -= l)
}

View file

@ -13,26 +13,27 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
val Tag = "ConnectThread"
val socket: BluetoothSocket =
val Socket: BluetoothSocket =
device.bluetoothDevice.createInsecureRfcommSocketToServiceRecord(ChatService.appUuid)
override def run(): Unit = {
Log.i(Tag, "Connecting to " + device.toString)
try {
socket.connect()
Socket.connect()
} catch {
case e: IOException =>
Log.w(Tag, "Failed to connect to " + device.toString, e)
try {
socket.close()
Socket.close()
} catch {
case e2: IOException =>
Log.e(Tag, "Failed to close socket", e2)
}
return;
return
}
Log.i(Tag, "Successfully connected to device " + device.name)
onConnected(device, socket)
onConnected(device, Socket)
}
}

View file

@ -1,7 +1,6 @@
package com.nutomic.ensichat.bluetooth
import android.bluetooth.BluetoothDevice
import org.msgpack.annotation.Message
object Device {
@ -10,7 +9,6 @@ object Device {
*
* @param Id A bluetooth device address.
*/
@Message
class ID(private val Id: String) {
override def hashCode = Id.hashCode
override def equals(a: Any) = a match {

View file

@ -7,6 +7,8 @@ import android.util.Log
/**
* Listens for incoming connections from other devices.
*
* @param name Service name to broadcast.
*/
class ListenThread(name: String, adapter: BluetoothAdapter,
onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
@ -23,7 +25,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
}
override def run(): Unit = {
Log.i(Tag, "Listening for connections")
Log.i(Tag, "Listening for connections at " + adapter.getAddress)
var socket: BluetoothSocket = null
while (true) {
@ -44,6 +46,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
}
def cancel(): Unit = {
Log.i(Tag, "Canceling listening")
try {
ServerSocket.close()
} catch {

View file

@ -1,20 +1,22 @@
package com.nutomic.ensichat.bluetooth
import java.io.{IOException, InputStream, OutputStream}
import java.io._
import java.util.Date
import android.bluetooth.BluetoothSocket
import android.util.Log
import com.nutomic.ensichat.messages.TextMessage
import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMessage}
/**
* Transfers data between connnected devices.
*
* @param device The bluetooth device to interact with.
* @param socket An open socket to the given device.
* @param encrypt 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,
onReceive: (TextMessage) => Unit) extends Thread {
class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Device.ID,
encrypt: Crypto, onReceive: (Message) => Unit) extends Thread {
val Tag: String = "TransferThread"
@ -38,11 +40,46 @@ class TransferThread(device: Device, socket: BluetoothSocket,
override def run(): Unit = {
Log.i(Tag, "Starting data transfer with " + device.toString)
// Keep listening to the InputStream while connected
while (true) {
try {
val msg = TextMessage.fromStream(InStream)
onReceive(msg)
val (msg, signature) = Message.read(InStream)
var messageValid = true
if (msg.sender != device.id) {
Log.i(Tag, "Dropping message with invalid sender from " + device.id)
messageValid = false
}
if (msg.receiver != localDevice) {
Log.i(Tag, "Dropping message with different receiver from " + device.id)
messageValid = false
}
// Add public key for new, local device.
// Explicitly check that message was not forwarded or spoofed.
if (msg.isInstanceOf[DeviceInfoMessage] && !encrypt.havePublicKey(msg.sender) &&
msg.sender == device.id) {
val dim = msg.asInstanceOf[DeviceInfoMessage]
// Permanently store public key for new local devices (also check signature).
if (msg.sender == device.id && encrypt.isValidSignature(msg, signature, dim.publicKey)) {
encrypt.addPublicKey(device.id, msg.asInstanceOf[DeviceInfoMessage].publicKey)
Log.i(Tag, "Added public key for new device " + device.name)
}
}
if (!encrypt.isValidSignature(msg, signature)) {
Log.i(Tag, "Dropping message with invalid signature from " + device.id)
messageValid = false
}
if (messageValid) {
msg match {
case m: TextMessage => onReceive(m)
case m: DeviceInfoMessage => encrypt.addPublicKey(msg.sender, m.publicKey)
}
}
} catch {
case e: IOException =>
Log.e(Tag, "Disconnected from device", e)
@ -51,9 +88,10 @@ class TransferThread(device: Device, socket: BluetoothSocket,
}
}
def send(message: TextMessage): Unit = {
def send(message: Message): Unit = {
try {
message.write(OutStream)
val sig = encrypt.calculateSignature(message)
message.write(OutStream, sig)
} catch {
case e: IOException =>
Log.e(Tag, "Failed to write message", e)

View file

@ -13,7 +13,7 @@ import android.widget._
import com.nutomic.ensichat.R
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.bluetooth.{ChatService, ChatServiceBinder, Device}
import com.nutomic.ensichat.messages.TextMessage
import com.nutomic.ensichat.messages.{Message, TextMessage}
import com.nutomic.ensichat.util.MessagesAdapter
import scala.collection.SortedSet
@ -108,7 +108,7 @@ class ChatFragment extends ListFragment with OnClickListener
val text: String = messageText.getText.toString
if (!text.isEmpty) {
chatService.send(
new TextMessage(chatService.localDeviceId, device, text.toString, new Date()))
new TextMessage(chatService.localDeviceId, device, new Date(), text.toString))
messageText.getText.clear()
}
}
@ -117,8 +117,9 @@ class ChatFragment extends ListFragment with OnClickListener
/**
* Displays new messages in UI.
*/
override def onMessageReceived(messages: SortedSet[TextMessage]): Unit = {
messages.foreach(m => adapter.add(m))
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
messages.filter(_.isInstanceOf[TextMessage])
.foreach(m => adapter.add(m.asInstanceOf[TextMessage]))
}
/**

View file

@ -0,0 +1,188 @@
package com.nutomic.ensichat.messages
import java.io.{File, FileInputStream, FileOutputStream, IOException}
import java.security._
import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
import android.content.Context
import android.util.Log
import com.nutomic.ensichat.bluetooth.Device
import com.nutomic.ensichat.messages.Crypto._
object Crypto {
/**
* Name of algorithm used for key generation.
*/
val KeyAlgorithm = "RSA"
/**
* Number of bits for local key pair.
*/
val KeySize = 2048
/**
* Name of algorithm used for message signing.
*/
val SignAlgorithm = "SHA256withRSA"
}
/**
* 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) {
private val Tag = "Crypto"
private val PrivateKeyAlias = "local-private"
private val PublicKeyAlias = "local-public"
/**
* 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()
saveKey(PrivateKeyAlias, keyPair.getPrivate)
saveKey(PublicKeyAlias, keyPair.getPublic)
}
/**
* 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()
/**
* Adds a new public key for a remote device.
*
* If a key for the device already exists, nothing is done.
*
* @param device The device to wchi 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)
} else {
Log.i(Tag, "Already have key for " + device.toString + ", not overwriting")
}
}
/**
* Checks if the message was properly signed.
*
* @param message The message to verify.
* @param signature The signature that was sent
* @return True if the signature is valid.
*/
def isValidSignature(message: Message, signature: Array[Byte], key: PublicKey = null): Boolean = {
val publicKey =
if (key != null) key
else loadKey(message.sender.toString, classOf[PublicKey])
val sig = Signature.getInstance(SignAlgorithm)
sig.initVerify(key)
sig.update(message.getBytes)
sig.verify(signature)
}
/**
* Returns a cryptographic signature for the given message (using local private key).
*/
def calculateSignature(message: Message): Array[Byte] = {
val sig = Signature.getInstance(SignAlgorithm)
val key = loadKey(PrivateKeyAlias, classOf[PrivateKey])
sig.initSign(key)
sig.update(message.getBytes)
sig.sign
}
/**
* Returns true if the local private and public key exist.
*/
def localKeysExist = new File(keyFolder, PublicKeyAlias).exists()
/**
* Returns the local public key.
*/
def getLocalPublicKey = loadKey(PublicKeyAlias, classOf[PublicKey])
/**
* Permanently stores the given key.
*
* The key can later be retrieved with [[loadKey]] and the same alias.
*
* @param alias Unique name under which the key should be stored.
* @param key The (private or public) key to store.
* @throws RuntimeException If a key with the given alias already exists.
*/
private def saveKey(alias: String, key: Key): Unit = {
val path = new File(keyFolder, alias)
if (path.exists()) {
throw new RuntimeException("Requested to overwrite existing key with alias " + alias +
", aborting")
}
keyFolder.mkdir()
var fos: Option[FileOutputStream] = None
try {
fos = Option(new FileOutputStream(path))
fos.foreach(_.write(key.getEncoded))
} catch {
case e: IOException => Log.w(Tag, "Failed to save key for alias " + alias, e)
} finally {
fos.foreach(_.close())
}
}
/**
* Loads a key that was stored with [[saveKey]].
*
* @param alias The alias under which the key was stored.
* @param keyType The type of key, either [[PrivateKey]] or [[PublicKey]].
* @tparam T Deduced from keyType.
* @return The key read from storage.
* @throws RuntimeException If the key does not exist.
*/
private def loadKey[T](alias: String, keyType: Class[T]): T = {
val path = new File(keyFolder, alias)
if (!path.exists()) {
throw new RuntimeException("The requested key with alias " + alias + " does not exist")
}
var fis: Option[FileInputStream] = None
var data: Array[Byte] = null
try {
fis = Option(new FileInputStream(path))
data = new Array[Byte](path.length().asInstanceOf[Int])
fis.foreach(_.read(data))
} catch {
case e: IOException => Log.e(Tag, "Failed to load key for alias " + alias, e)
} finally {
fis.foreach(_.close())
}
val keyFactory = KeyFactory.getInstance(KeyAlgorithm)
keyType match {
case c if c == classOf[PublicKey] =>
val keySpec = new X509EncodedKeySpec(data)
keyFactory.generatePublic(keySpec).asInstanceOf[T]
case c if c == classOf[PrivateKey] =>
val keySpec = new PKCS8EncodedKeySpec(data)
keyFactory.generatePrivate(keySpec).asInstanceOf[T]
}
}
/**
* Returns the folder where keys are stored.
*/
private def keyFolder = new File(filesDir, "keys")
}

View file

@ -0,0 +1,45 @@
package com.nutomic.ensichat.messages
import java.security.spec.X509EncodedKeySpec
import java.security.{KeyFactory, PublicKey}
import java.util.Date
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 == publicKey
override def hashCode = super.hashCode + publicKey.hashCode
override def toString = "DeviceInfoMessage(" + sender.toString + ", " + receiver.toString +
", " + date.toString + ", " + publicKey.toString + ")"
override def getBytes = super.getBytes ++ publicKey.getEncoded
}

View file

@ -0,0 +1,125 @@
package com.nutomic.ensichat.messages
import java.io.{InputStream, OutputStream}
import java.nio.ByteBuffer
import java.util.Date
import com.nutomic.ensichat.bluetooth.Device
import org.msgpack.ScalaMessagePack
import org.msgpack.packer.Packer
object Message {
/**
* Types of messages that can be transfered.
*
* There must be one type for each implementation.
*/
object Type {
val Text = 1
val DeviceInfo = 2
}
/**
* Orders messages by date, oldest messages first.
*/
val Ordering = new Ordering[Message] {
override def compare(m1: Message, m2: Message) = m1.date.compareTo(m2.date)
}
/**
* Deserializes a stream that was written by [[Message.write]] into the correct subclass.
*
* @return Deserialized message and sits signature.
*/
def read(in: InputStream): (Message, Array[Byte]) = {
val up = new ScalaMessagePack().createUnpacker(in)
val messageType = up.readInt()
val sender = new Device.ID(up.readString())
val receiver = new Device.ID(up.readString())
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)
}, sig)
}
}
/**
* Message object that can be sent between remote devices.
*
* @param messageType One of [[Message.Type]].
*/
abstract class Message(messageType: Int) {
/**
* Device where the message was sent from.
*/
val sender: Device.ID
/**
* Device the message is addressed to.
*/
val receiver: Device.ID
/**
* Timestamp of message creation.
*/
val date: Date
/**
* Serializes this message and the given signature into stream.
*/
def write(os: OutputStream, signature: Array[Byte]): Unit = {
val packer = new ScalaMessagePack().createPacker(os)
.write(messageType)
.write(sender.toString)
.write(receiver.toString)
.write(date.getTime)
.write(signature)
doWrite(packer)
}
/**
* Serializes any extra data for implementing classes.
*/
protected def doWrite(packer: Packer): Unit
/**
* Returns true if objects are equal.
*
* Implementations must provide their own implementation to check the result of this
* function and their own data.
*/
override def equals(a: Any): Boolean = a match {
case o: TextMessage =>
sender == o.sender && receiver == o.receiver && date == o.date
case _ => false
}
/**
* Returns a hash code for this object.
*
* Implementations must provide their own implementation to check the result of this
* function and their own data.
*/
override def hashCode: Int = sender.hashCode + receiver.hashCode + date.hashCode
override def toString: String
/**
* Returns this object's data encoded as Array[Byte].
*
* Implementations must provide their own implementation to check the result of this
* function and their own data.
*/
def getBytes: Array[Byte] = intToBytes(messageType) ++ sender.toString.getBytes ++
receiver.toString.getBytes ++ longToBytes(date.getTime)
private def intToBytes(i: Int) = ByteBuffer.allocate(java.lang.Integer.SIZE / 8).putInt(i).array()
private def longToBytes(l: Long) = ByteBuffer.allocate(java.lang.Long.SIZE / 8).putLong(l).array()
}

View file

@ -40,18 +40,18 @@ class MessageStore(context: Context) extends SQLiteOpenHelper(context, MessageSt
/**
* Returns the count last messages for device.
*/
def getMessages(device: Device.ID, count: Int): SortedSet[TextMessage] = {
def getMessages(device: Device.ID, count: Int): SortedSet[Message] = {
val c: Cursor = getReadableDatabase.query(true,
"messages", Array("sender", "receiver", "text", "date"),
"sender = ? OR receiver = ?", Array(device.toString, device.toString),
null, null, "date DESC", count.toString)
var messages: SortedSet[TextMessage] = new TreeSet[TextMessage]()(TextMessage.Ordering)
var messages: SortedSet[Message] = new TreeSet[Message]()(Message.Ordering)
while (c.moveToNext()) {
val m: TextMessage = new TextMessage(
new Device.ID(c.getString(c.getColumnIndex("sender"))),
new Device.ID(c.getString(c.getColumnIndex("receiver"))),
new String(c.getBlob(c.getColumnIndex ("text"))),
new Date(c.getLong(c.getColumnIndex("date"))))
new Date(c.getLong(c.getColumnIndex("date"))),
new String(c.getString(c.getColumnIndex ("text"))))
messages += m
}
c.close()
@ -61,13 +61,16 @@ class MessageStore(context: Context) extends SQLiteOpenHelper(context, MessageSt
/**
* Inserts the given new message into the database.
*/
def addMessage(message: TextMessage): Unit = {
def addMessage(message: Message): Unit = message match {
case msg: TextMessage =>
val cv: ContentValues = new ContentValues()
cv.put("sender", message.sender.toString)
cv.put("receiver", message.receiver.toString)
cv.put("text", message.text)
cv.put("date", message.date.getTime.toString) // toString used as workaround for compile error
cv.put("sender", msg.sender.toString)
cv.put("receiver", msg.receiver.toString)
// toString used as workaround for compile error with Long.
cv.put("date", msg.date.getTime.toString)
cv.put("text", msg.text)
getWritableDatabase.insert("messages", null, cv)
case msg: DeviceInfoMessage => // Never stored.
}
override def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {

View file

@ -1,56 +1,34 @@
package com.nutomic.ensichat.messages
import java.io.{InputStream, OutputStream}
import java.util.Date
import com.nutomic.ensichat.bluetooth.Device
import org.msgpack.ScalaMessagePack
import com.nutomic.ensichat.messages.Message._
import org.msgpack.packer.Packer
import org.msgpack.unpacker.Unpacker
object TextMessage {
val Ordering = new Ordering[TextMessage] {
override def compare(m1: TextMessage, m2: TextMessage) = m1.date.compareTo(m2.date)
}
/**
* Constructs a new message from stream.
*/
def fromStream(in: InputStream): TextMessage = {
val up = new ScalaMessagePack().createUnpacker(in)
new TextMessage(
new Device.ID(up.read(classOf[String])),
new Device.ID(up.read(classOf[String])),
up.read(classOf[String]),
new Date(up.read(classOf[Long])))
}
def read(sender: Device.ID, receiver: Device.ID, date: Date, up: Unpacker): TextMessage =
new TextMessage(sender, receiver, date, up.readString())
}
/**
* Represents content and metadata that can be transferred between devices.
* Message that contains text.
*/
class TextMessage(val sender: Device.ID, val receiver: Device.ID,
val text: String, val date: Date) {
class TextMessage(override val sender: Device.ID, override val receiver: Device.ID,
override val date: Date, val text: String) extends Message(Type.Text) {
/**
* Writes this object into stream.
*/
def write(os: OutputStream): Unit = {
new ScalaMessagePack().createPacker(os)
.write(sender.toString)
.write(receiver.toString)
.write(text)
.write(date.getTime)
}
override def doWrite(packer: Packer) = packer.write(text)
override def equals(a: Any) = a match {
case o: TextMessage =>
sender == o.sender && receiver == o.receiver && text == o.text && date == o.date
case _ => false
}
override def equals(a: Any) = super.equals(a) && a.asInstanceOf[TextMessage].text == text
override def hashCode() = sender.hashCode + receiver.hashCode + text.hashCode + date.hashCode()
override def hashCode = super.hashCode + text.hashCode
override def toString = "TextMessage(" + sender.toString + ", " + receiver.toString +
", " + text + ", " + date.toString + ")"
", " + date.toString + ", " + text + ")"
override def getBytes = super.getBytes ++ text.getBytes
}