(De-)serialize message to/from byte array instead of stream.

This lets us get rid of seperate `getBytes` function for signing,
and is needed for end to end encryption.
This commit is contained in:
Felix Ableitner 2014-11-06 17:15:05 +02:00
parent 4cbaf975b5
commit 5e26fcce75
8 changed files with 41 additions and 46 deletions

View File

@ -1,8 +1,8 @@
package com.nutomic.ensichat.messages package com.nutomic.ensichat.messages
import android.test.AndroidTestCase import android.test.AndroidTestCase
import junit.framework.Assert._
import com.nutomic.ensichat.messages.MessageTest._ import com.nutomic.ensichat.messages.MessageTest._
import junit.framework.Assert._
class CryptoTest extends AndroidTestCase { class CryptoTest extends AndroidTestCase {

View File

@ -8,8 +8,8 @@ import android.database.sqlite.SQLiteDatabase
import android.test.AndroidTestCase import android.test.AndroidTestCase
import android.test.mock.MockContext import android.test.mock.MockContext
import com.nutomic.ensichat.bluetooth.Device import com.nutomic.ensichat.bluetooth.Device
import junit.framework.Assert._
import com.nutomic.ensichat.messages.MessageTest._ import com.nutomic.ensichat.messages.MessageTest._
import junit.framework.Assert._
class MessageStoreTest extends AndroidTestCase { class MessageStoreTest extends AndroidTestCase {

View File

@ -29,8 +29,8 @@ class MessageTest extends AndroidTestCase {
def testSerialize(): Unit = { def testSerialize(): Unit = {
val pis = new PipedInputStream() val pis = new PipedInputStream()
val pos = new PipedOutputStream(pis) val pos = new PipedOutputStream(pis)
m1.write(pos, Array[Byte]()) val bytes = m1.write(Array[Byte]())
val (msg, _) = Message.read(pis) val (msg, _) = Message.read(bytes)
assertEquals(m1, msg) assertEquals(m1, msg)
} }

View File

@ -1,7 +1,6 @@
package com.nutomic.ensichat.bluetooth package com.nutomic.ensichat.bluetooth
import java.io._ import java.io._
import java.util.Date
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.util.Log import android.util.Log
@ -10,6 +9,8 @@ import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMe
/** /**
* Transfers data between connnected devices. * Transfers data between connnected devices.
* *
* Messages must not be longer than [[TransferThread#MaxMessageLength]] bytes.
*
* @param device The bluetooth device to interact with. * @param device The bluetooth device to interact with.
* @param socket An open socket to the given device. * @param socket An open socket to the given device.
* @param encrypt Object used to handle signing and encryption of messages. * @param encrypt Object used to handle signing and encryption of messages.
@ -18,7 +19,9 @@ import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMe
class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Device.ID, class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Device.ID,
encrypt: Crypto, onReceive: (Message) => Unit) extends Thread { encrypt: Crypto, onReceive: (Message) => Unit) extends Thread {
val Tag: String = "TransferThread" private val Tag: String = "TransferThread"
private val MaxMessageLength = 4096
val InStream: InputStream = val InStream: InputStream =
try { try {
@ -44,7 +47,9 @@ class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Devic
// Keep listening to the InputStream while connected // Keep listening to the InputStream while connected
while (true) { while (true) {
try { try {
val (msg, signature) = Message.read(InStream) val bytes = new Array[Byte](MaxMessageLength)
InStream.read(bytes)
val (msg, signature) = Message.read(bytes)
var messageValid = true var messageValid = true
if (msg.sender != device.id) { if (msg.sender != device.id) {
@ -57,14 +62,14 @@ class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Devic
messageValid = false messageValid = false
} }
// Add public key for new, local device. // Add public key for new, directly connected device.
// Explicitly check that message was not forwarded or spoofed. // Explicitly check that message was not forwarded or spoofed.
if (msg.isInstanceOf[DeviceInfoMessage] && !encrypt.havePublicKey(msg.sender) && if (msg.isInstanceOf[DeviceInfoMessage] && !encrypt.havePublicKey(msg.sender) &&
msg.sender == device.id) { msg.sender == device.id) {
val dim = msg.asInstanceOf[DeviceInfoMessage] val dim = msg.asInstanceOf[DeviceInfoMessage]
// Permanently store public key for new local devices (also check signature). // Permanently store public key for new local devices (also check signature).
if (msg.sender == device.id && encrypt.isValidSignature(msg, signature, dim.publicKey)) { if (encrypt.isValidSignature(msg, signature, dim.publicKey)) {
encrypt.addPublicKey(device.id, msg.asInstanceOf[DeviceInfoMessage].publicKey) encrypt.addPublicKey(device.id, dim.publicKey)
Log.i(Tag, "Added public key for new device " + device.name) Log.i(Tag, "Added public key for new device " + device.name)
} }
} }
@ -91,7 +96,8 @@ class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Devic
def send(message: Message): Unit = { def send(message: Message): Unit = {
try { try {
val sig = encrypt.calculateSignature(message) val sig = encrypt.calculateSignature(message)
message.write(OutStream, sig) val bytes = message.write(sig)
OutStream.write(bytes)
} catch { } catch {
case e: IOException => case e: IOException =>
Log.e(Tag, "Failed to write message", e) Log.e(Tag, "Failed to write message", e)

View File

@ -4,7 +4,6 @@ import java.io.{File, FileInputStream, FileOutputStream, IOException}
import java.security._ import java.security._
import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
import android.content.Context
import android.util.Log import android.util.Log
import com.nutomic.ensichat.bluetooth.Device import com.nutomic.ensichat.bluetooth.Device
import com.nutomic.ensichat.messages.Crypto._ import com.nutomic.ensichat.messages.Crypto._
@ -80,6 +79,8 @@ class Crypto(filesDir: File) {
/** /**
* Checks if the message was properly signed. * Checks if the message was properly signed.
* *
* This is done by signing the output of [[Message.write()]] called with an empty signature.
*
* @param message The message to verify. * @param message The message to verify.
* @param signature The signature that was sent * @param signature The signature that was sent
* @return True if the signature is valid. * @return True if the signature is valid.
@ -89,19 +90,21 @@ class Crypto(filesDir: File) {
if (key != null) key if (key != null) key
else loadKey(message.sender.toString, classOf[PublicKey]) else loadKey(message.sender.toString, classOf[PublicKey])
val sig = Signature.getInstance(SignAlgorithm) val sig = Signature.getInstance(SignAlgorithm)
sig.initVerify(key) sig.initVerify(publicKey)
sig.update(message.getBytes) sig.update(message.write(Array[Byte]()))
sig.verify(signature) sig.verify(signature)
} }
/** /**
* Returns a cryptographic signature for the given message (using local private key). * Returns a cryptographic signature for the given message (using local private key).
*
* This is done by signing the output of [[Message.write()]] called with an empty signature.
*/ */
def calculateSignature(message: Message): Array[Byte] = { def calculateSignature(message: Message): Array[Byte] = {
val sig = Signature.getInstance(SignAlgorithm) val sig = Signature.getInstance(SignAlgorithm)
val key = loadKey(PrivateKeyAlias, classOf[PrivateKey]) val key = loadKey(PrivateKeyAlias, classOf[PrivateKey])
sig.initSign(key) sig.initSign(key)
sig.update(message.getBytes) sig.update(message.write(Array[Byte]()))
sig.sign sig.sign
} }

View File

@ -40,6 +40,4 @@ class DeviceInfoMessage(override val sender: Device.ID, override val receiver: D
override def toString = "DeviceInfoMessage(" + sender.toString + ", " + receiver.toString + override def toString = "DeviceInfoMessage(" + sender.toString + ", " + receiver.toString +
", " + date.toString + ", " + publicKey.toString + ")" ", " + date.toString + ", " + publicKey.toString + ")"
override def getBytes = super.getBytes ++ publicKey.getEncoded
} }

View File

@ -1,7 +1,5 @@
package com.nutomic.ensichat.messages package com.nutomic.ensichat.messages
import java.io.{InputStream, OutputStream}
import java.nio.ByteBuffer
import java.util.Date import java.util.Date
import com.nutomic.ensichat.bluetooth.Device import com.nutomic.ensichat.bluetooth.Device
@ -28,17 +26,19 @@ object Message {
} }
/** /**
* Deserializes a stream that was written by [[Message.write]] into the correct subclass. * Reads a byte array that was written by [[Message.write]] into the correct
* message implementation..
* *
* @return Deserialized message and sits signature. * @return Deserialized message and sits signature.
*/ */
def read(in: InputStream): (Message, Array[Byte]) = { def read(bytes: Array[Byte]): (Message, Array[Byte]) = {
val up = new ScalaMessagePack().createUnpacker(in) val up = new ScalaMessagePack().createBufferUnpacker(bytes)
val messageType = up.readInt() val messageType = up.readInt()
val sender = new Device.ID(up.readString()) val sender = new Device.ID(up.readString())
val receiver = new Device.ID(up.readString()) val receiver = new Device.ID(up.readString())
val date = new Date(up.readLong()) val date = new Date(up.readLong())
val sig = up.readByteArray() val sig = up.readByteArray()
(messageType match { (messageType match {
case Type.Text => TextMessage.read(sender, receiver, date, up) case Type.Text => TextMessage.read(sender, receiver, date, up)
case Type.DeviceInfo => DeviceInfoMessage.read(sender, receiver, date, up) case Type.DeviceInfo => DeviceInfoMessage.read(sender, receiver, date, up)
@ -70,16 +70,19 @@ abstract class Message(messageType: Int) {
val date: Date val date: Date
/** /**
* Serializes this message and the given signature into stream. * Writes this message and the given signature into byte array.
*
* Signature may not be null, but can be an empty array.
*/ */
def write(os: OutputStream, signature: Array[Byte]): Unit = { def write(signature: Array[Byte]): Array[Byte] = {
val packer = new ScalaMessagePack().createPacker(os) val packer = new ScalaMessagePack().createBufferPacker()
.write(messageType) packer.write(messageType)
.write(sender.toString) .write(sender.toString)
.write(receiver.toString) .write(receiver.toString)
.write(date.getTime) .write(date.getTime)
.write(signature) .write(signature)
doWrite(packer) doWrite(packer)
packer.toByteArray
} }
/** /**
@ -109,17 +112,4 @@ abstract class Message(messageType: Int) {
override def toString: String 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

@ -29,6 +29,4 @@ class TextMessage(override val sender: Device.ID, override val receiver: Device.
override def toString = "TextMessage(" + sender.toString + ", " + receiver.toString + override def toString = "TextMessage(" + sender.toString + ", " + receiver.toString +
", " + date.toString + ", " + text + ")" ", " + date.toString + ", " + text + ")"
override def getBytes = super.getBytes ++ text.getBytes
} }