(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
import android.test.AndroidTestCase
import junit.framework.Assert._
import com.nutomic.ensichat.messages.MessageTest._
import junit.framework.Assert._
class CryptoTest extends AndroidTestCase {

View File

@ -8,8 +8,8 @@ import android.database.sqlite.SQLiteDatabase
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._
import junit.framework.Assert._
class MessageStoreTest extends AndroidTestCase {

View File

@ -29,8 +29,8 @@ 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)
val bytes = m1.write(Array[Byte]())
val (msg, _) = Message.read(bytes)
assertEquals(m1, msg)
}

View File

@ -1,7 +1,6 @@
package com.nutomic.ensichat.bluetooth
import java.io._
import java.util.Date
import android.bluetooth.BluetoothSocket
import android.util.Log
@ -10,6 +9,8 @@ import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMe
/**
* Transfers data between connnected devices.
*
* Messages must not be longer than [[TransferThread#MaxMessageLength]] bytes.
*
* @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.
@ -18,7 +19,9 @@ import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMe
class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Device.ID,
encrypt: Crypto, onReceive: (Message) => Unit) extends Thread {
val Tag: String = "TransferThread"
private val Tag: String = "TransferThread"
private val MaxMessageLength = 4096
val InStream: InputStream =
try {
@ -44,7 +47,9 @@ class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Devic
// Keep listening to the InputStream while connected
while (true) {
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
if (msg.sender != device.id) {
@ -57,14 +62,14 @@ class TransferThread(device: Device, socket: BluetoothSocket, localDevice: Devic
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.
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)
if (encrypt.isValidSignature(msg, signature, dim.publicKey)) {
encrypt.addPublicKey(device.id, dim.publicKey)
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 = {
try {
val sig = encrypt.calculateSignature(message)
message.write(OutStream, sig)
val bytes = message.write(sig)
OutStream.write(bytes)
} catch {
case e: IOException =>
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.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
import android.content.Context
import android.util.Log
import com.nutomic.ensichat.bluetooth.Device
import com.nutomic.ensichat.messages.Crypto._
@ -80,6 +79,8 @@ class Crypto(filesDir: File) {
/**
* Checks if the message was properly signed.
*
* This is done by signing the output of [[Message.write()]] called with an empty signature.
*
* @param message The message to verify.
* @param signature The signature that was sent
* @return True if the signature is valid.
@ -89,19 +90,21 @@ class Crypto(filesDir: File) {
if (key != null) key
else loadKey(message.sender.toString, classOf[PublicKey])
val sig = Signature.getInstance(SignAlgorithm)
sig.initVerify(key)
sig.update(message.getBytes)
sig.initVerify(publicKey)
sig.update(message.write(Array[Byte]()))
sig.verify(signature)
}
/**
* Returns a cryptographic signature for the given message (using local private key).
*
* This is done by signing the output of [[Message.write()]] called with an empty signature.
*/
def calculateSignature(message: Message): Array[Byte] = {
val sig = Signature.getInstance(SignAlgorithm)
val key = loadKey(PrivateKeyAlias, classOf[PrivateKey])
sig.initSign(key)
sig.update(message.getBytes)
sig.update(message.write(Array[Byte]()))
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 +
", " + date.toString + ", " + publicKey.toString + ")"
override def getBytes = super.getBytes ++ publicKey.getEncoded
}

View File

@ -1,7 +1,5 @@
package com.nutomic.ensichat.messages
import java.io.{InputStream, OutputStream}
import java.nio.ByteBuffer
import java.util.Date
import com.nutomic.ensichat.bluetooth.Device
@ -28,12 +26,14 @@ 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.
*/
def read(in: InputStream): (Message, Array[Byte]) = {
val up = new ScalaMessagePack().createUnpacker(in)
def read(bytes: Array[Byte]): (Message, Array[Byte]) = {
val up = new ScalaMessagePack().createBufferUnpacker(bytes)
val messageType = up.readInt()
val sender = new Device.ID(up.readString())
val receiver = new Device.ID(up.readString())
@ -70,16 +70,19 @@ abstract class Message(messageType: Int) {
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 = {
val packer = new ScalaMessagePack().createPacker(os)
.write(messageType)
def write(signature: Array[Byte]): Array[Byte] = {
val packer = new ScalaMessagePack().createBufferPacker()
packer.write(messageType)
.write(sender.toString)
.write(receiver.toString)
.write(date.getTime)
.write(signature)
doWrite(packer)
packer.toByteArray
}
/**
@ -109,17 +112,4 @@ abstract class Message(messageType: Int) {
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 +
", " + date.toString + ", " + text + ")"
override def getBytes = super.getBytes ++ text.getBytes
}