From b4f5569ec95f5001022203c250c6e2efc4925ae9 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Sun, 9 Nov 2014 02:08:36 +0200 Subject: [PATCH] Added end-to-end message encryption. --- .../CryptoTest.scala | 25 +++-- .../MessageStoreTest.scala | 2 - .../MessageTest.scala | 1 - .../ensichat/activities/MainActivity.scala | 1 - .../ensichat/bluetooth/ChatService.scala | 10 +- .../ensichat/bluetooth/TransferThread.scala | 24 ++--- .../nutomic/ensichat/messages/Crypto.scala | 94 ++++++++++++++++++- .../ensichat/messages/DeviceInfoMessage.scala | 2 +- .../nutomic/ensichat/messages/Message.scala | 3 +- 9 files changed, 131 insertions(+), 31 deletions(-) diff --git a/app/src/androidTest/scala/com.nutomic.ensichat.messages/CryptoTest.scala b/app/src/androidTest/scala/com.nutomic.ensichat.messages/CryptoTest.scala index b2bf18b..fa209e8 100644 --- a/app/src/androidTest/scala/com.nutomic.ensichat.messages/CryptoTest.scala +++ b/app/src/androidTest/scala/com.nutomic.ensichat.messages/CryptoTest.scala @@ -1,24 +1,37 @@ 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 { - var encrypt: Crypto = _ + lazy val Crypto: Crypto = new Crypto(getContext.getFilesDir) override def setUp(): Unit = { super.setUp() - encrypt = new Crypto(getContext.getFilesDir) - if (!encrypt.localKeysExist) { - encrypt.generateLocalKeys() + if (!Crypto.localKeysExist) { + Crypto.generateLocalKeys() } } def testSignVerify(): Unit = { - val sig = encrypt.calculateSignature(m1) - assertTrue(encrypt.isValidSignature(m1, sig, encrypt.getLocalPublicKey)) + val sig = Crypto.calculateSignature(m1) + assertTrue(Crypto.isValidSignature(m1, sig, Crypto.getLocalPublicKey)) + } + + 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 decrypted = Crypto.decrypt(encrypted, key) + val out = Message.read(decrypted)._1.asInstanceOf[DeviceInfoMessage] + + assertEquals(in, out) } } diff --git a/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageStoreTest.scala b/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageStoreTest.scala index 43545f8..bb9e99b 100644 --- a/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageStoreTest.scala +++ b/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageStoreTest.scala @@ -7,8 +7,6 @@ import android.database.DatabaseErrorHandler import android.database.sqlite.SQLiteDatabase import android.test.AndroidTestCase import android.test.mock.MockContext -import android.util.Log -import com.nutomic.ensichat.bluetooth.Device import com.nutomic.ensichat.messages.MessageTest._ import junit.framework.Assert._ diff --git a/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageTest.scala b/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageTest.scala index 2802570..66575d5 100644 --- a/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageTest.scala +++ b/app/src/androidTest/scala/com.nutomic.ensichat.messages/MessageTest.scala @@ -21,7 +21,6 @@ object MessageTest { val m3 = new TextMessage(new Device.ID("DD:DD:DD:DD:DD:DD"), new Device.ID("BB:BB:BB:BB:BB:BB"), new GregorianCalendar(2014, 10, 31).getTime, "third") - } class MessageTest extends AndroidTestCase { diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala index cd2d781..ab2012c 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/MainActivity.scala @@ -4,7 +4,6 @@ import android.app.Activity import android.bluetooth.BluetoothAdapter import android.content._ import android.os.Bundle -import android.view.{Menu, MenuItem} import android.widget.Toast import com.nutomic.ensichat.R import com.nutomic.ensichat.bluetooth.{ChatService, Device} diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala index 198c9a3..0adfcdf 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/ChatService.scala @@ -70,7 +70,7 @@ class ChatService extends Service { private var MessageStore: MessageStore = _ - private lazy val Encrypt = new Crypto(getFilesDir) + private lazy val Crypto = new Crypto(getFilesDir) /** * Initializes BroadcastReceiver for discovery, starts discovery and listens for connections. @@ -88,10 +88,10 @@ class ChatService extends Service { startBluetoothConnections() } - if (!Encrypt.localKeysExist) { + if (!Crypto.localKeysExist) { new Thread(new Runnable { override def run(): Unit = { - Encrypt.generateLocalKeys() + Crypto.generateLocalKeys() } }).start() } @@ -194,9 +194,9 @@ class ChatService extends Service { if (device.connected) { connections += (device.id -> - new TransferThread(device, socket, this, Encrypt, handleNewMessage)) + new TransferThread(device, socket, this, Crypto, handleNewMessage)) connections(device.id).start() - send(new DeviceInfoMessage(localDeviceId, device.id, new Date(), Encrypt.getLocalPublicKey)) + send(new DeviceInfoMessage(localDeviceId, device.id, new Date(), Crypto.getLocalPublicKey)) } connectionListeners.foreach(l => l.get match { diff --git a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala index fdfaa82..a020111 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala @@ -5,6 +5,7 @@ import java.io._ import android.bluetooth.BluetoothSocket import android.util.Log import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMessage} +import org.msgpack.ScalaMessagePack /** * Transfers data between connnected devices. @@ -21,8 +22,6 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi private val Tag: String = "TransferThread" - private val MaxMessageLength = 4096 - val InStream: InputStream = try { socket.getInputStream @@ -46,9 +45,11 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi while (socket.isConnected) { try { - val bytes = new Array[Byte](MaxMessageLength) - InStream.read(bytes) - val (message, signature) = Message.read(bytes) + val up = new ScalaMessagePack().createUnpacker(InStream) + val encrypted = up.readByteArray() + val key = up.readByteArray() + val plain = crypto.decrypt(encrypted, key) + val (message, signature) = Message.read(plain) var messageValid = true if (message.sender != device.id) { @@ -97,11 +98,13 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi def send(message: Message): Unit = { try { val sig = crypto.calculateSignature(message) - val bytes = message.write(sig) - OutStream.write(bytes) + val plain = message.write(sig) + val (encrypted, key) = crypto.encrypt(message.receiver, plain) + new ScalaMessagePack().createPacker(OutStream) + .write(encrypted) + .write(key) } catch { - case e: IOException => - Log.e(Tag, "Failed to write message", e) + case e: IOException => Log.e(Tag, "Failed to write message", e) } } @@ -109,8 +112,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi try { socket.close() } catch { - case e: IOException => - Log.e(Tag, "Failed to close socket", e); + case e: IOException => Log.e(Tag, "Failed to close socket", e); } } diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala b/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala index cb97b60..963e7c9 100644 --- a/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala +++ b/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala @@ -1,8 +1,10 @@ package com.nutomic.ensichat.messages -import java.io.{File, FileInputStream, FileOutputStream, IOException} +import java.io._ import java.security._ import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} +import javax.crypto.spec.SecretKeySpec +import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey} import android.util.Log import com.nutomic.ensichat.bluetooth.Device @@ -21,10 +23,20 @@ object Crypto { val KeySize = 2048 /** - * Name of algorithm used for message signing. + * Algorithm used for message signing. */ val SignAlgorithm = "SHA256withRSA" + /** + * Algorithm used for symmetric crypto cipher. + */ + val SymmetricCipherAlgorithm = "AES" + + /** + * Algorithm used for symmetric message encryption. + */ + val SymmetricKeyAlgorithm = "AES/CBC/PKCS5Padding" + } /** @@ -188,4 +200,82 @@ class Crypto(filesDir: File) { */ private def keyFolder = new File(filesDir, "keys") + /** + * Encrypts data for the given receiver. + * + * @param receiver The device that should be able to decrypt this message. + * @param data The message to encrypt. + * @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): + (Array[Byte], Array[Byte]) = { + // Symmetric encryption of data + val secretKey = makeSecretKey() + val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm) + symmetricCipher.init(Cipher.ENCRYPT_MODE, secretKey) + val encryptedData = copyThroughCipher(symmetricCipher, data) + + // Asymmetric encryption of secret key + val publicKey = + if (key != null) key + else loadKey(receiver.toString, classOf[PublicKey]) + val asymmetricCipher = Cipher.getInstance(KeyAlgorithm) + asymmetricCipher.init(Cipher.WRAP_MODE, publicKey) + + (encryptedData, asymmetricCipher.wrap(secretKey)) + } + + /** + * Decrypts the output of [[encrypt]]. + * + * @param data The AES encrypted data to decrypt. + * @param key The RSA encrypted AES key used to encrypt data. + * @return The plain text data. + */ + def decrypt(data: Array[Byte], key: Array[Byte]): Array[Byte] = { + // Asymmetric decryption of secret key + val asymmetricCipher = Cipher.getInstance(KeyAlgorithm) + asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey])) + val secretKey = asymmetricCipher.unwrap(key, SymmetricKeyAlgorithm, Cipher.SECRET_KEY) + + // Symmetric decryption of data + val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm) + symmetricCipher.init(Cipher.DECRYPT_MODE, secretKey) + val dec = copyThroughCipher(symmetricCipher, data) + dec + } + + /** + * Passes data through cipher stream to encrypt or decrypt it and returns int. + * + * Operation mode depends on the parameters to [[Cipher#init]]. + * + * @param cipher An initialized cipher. + * @param data The data to encrypt or decrypt. + * @return The encrypted or decrypted data. + */ + private def copyThroughCipher(cipher: Cipher, data: Array[Byte]): Array[Byte] = { + val bais = new ByteArrayInputStream(data) + val baos = new ByteArrayOutputStream() + val cos = new CipherOutputStream(baos, cipher) + var i = 0 + val b = new Array[Byte](1024) + while({i = bais.read(b); i != -1}) { + cos.write(b, 0, i) + } + baos.write(cipher.doFinal()) + baos.toByteArray + } + + /** + * Creates a new, random AES key. + */ + private def makeSecretKey(): SecretKey = { + val kgen = KeyGenerator.getInstance(SymmetricCipherAlgorithm) + kgen.init(256) + val key = kgen.generateKey() + new SecretKeySpec(key.getEncoded, SymmetricKeyAlgorithm) + } + } diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/DeviceInfoMessage.scala b/app/src/main/scala/com/nutomic/ensichat/messages/DeviceInfoMessage.scala index d23d4ee..3045255 100644 --- a/app/src/main/scala/com/nutomic/ensichat/messages/DeviceInfoMessage.scala +++ b/app/src/main/scala/com/nutomic/ensichat/messages/DeviceInfoMessage.scala @@ -33,7 +33,7 @@ class DeviceInfoMessage(override val sender: Device.ID, override val receiver: D override def doWrite(packer: Packer) = packer.write(publicKey.getEncoded) override def equals(a: Any) = - super.equals(a) && a.asInstanceOf[DeviceInfoMessage].publicKey == publicKey + super.equals(a) && a.asInstanceOf[DeviceInfoMessage].publicKey.toString == publicKey.toString override def hashCode = super.hashCode + publicKey.hashCode diff --git a/app/src/main/scala/com/nutomic/ensichat/messages/Message.scala b/app/src/main/scala/com/nutomic/ensichat/messages/Message.scala index 78f8a37..a012ff2 100644 --- a/app/src/main/scala/com/nutomic/ensichat/messages/Message.scala +++ b/app/src/main/scala/com/nutomic/ensichat/messages/Message.scala @@ -97,8 +97,7 @@ abstract class Message(messageType: Int) { * 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 o: Message => sender == o.sender && receiver == o.receiver && date == o.date case _ => false }