(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:
parent
4cbaf975b5
commit
5e26fcce75
8 changed files with 41 additions and 46 deletions
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue