Added end-to-end message encryption.

This commit is contained in:
Felix Ableitner 2014-11-09 02:08:36 +02:00
parent 6b2ef30888
commit b4f5569ec9
9 changed files with 131 additions and 31 deletions

View file

@ -1,24 +1,37 @@
package com.nutomic.ensichat.messages package com.nutomic.ensichat.messages
import java.util.GregorianCalendar
import android.test.AndroidTestCase import android.test.AndroidTestCase
import com.nutomic.ensichat.bluetooth.Device
import com.nutomic.ensichat.messages.MessageTest._ import com.nutomic.ensichat.messages.MessageTest._
import junit.framework.Assert._ import junit.framework.Assert._
class CryptoTest extends AndroidTestCase { class CryptoTest extends AndroidTestCase {
var encrypt: Crypto = _ lazy val Crypto: Crypto = new Crypto(getContext.getFilesDir)
override def setUp(): Unit = { override def setUp(): Unit = {
super.setUp() super.setUp()
encrypt = new Crypto(getContext.getFilesDir) if (!Crypto.localKeysExist) {
if (!encrypt.localKeysExist) { Crypto.generateLocalKeys()
encrypt.generateLocalKeys()
} }
} }
def testSignVerify(): Unit = { def testSignVerify(): Unit = {
val sig = encrypt.calculateSignature(m1) val sig = Crypto.calculateSignature(m1)
assertTrue(encrypt.isValidSignature(m1, sig, encrypt.getLocalPublicKey)) 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)
} }
} }

View file

@ -7,8 +7,6 @@ import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.test.AndroidTestCase import android.test.AndroidTestCase
import android.test.mock.MockContext import android.test.mock.MockContext
import android.util.Log
import com.nutomic.ensichat.bluetooth.Device
import com.nutomic.ensichat.messages.MessageTest._ import com.nutomic.ensichat.messages.MessageTest._
import junit.framework.Assert._ import junit.framework.Assert._

View file

@ -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"), 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") new GregorianCalendar(2014, 10, 31).getTime, "third")
} }
class MessageTest extends AndroidTestCase { class MessageTest extends AndroidTestCase {

View file

@ -4,7 +4,6 @@ import android.app.Activity
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content._ import android.content._
import android.os.Bundle import android.os.Bundle
import android.view.{Menu, MenuItem}
import android.widget.Toast import android.widget.Toast
import com.nutomic.ensichat.R import com.nutomic.ensichat.R
import com.nutomic.ensichat.bluetooth.{ChatService, Device} import com.nutomic.ensichat.bluetooth.{ChatService, Device}

View file

@ -70,7 +70,7 @@ class ChatService extends Service {
private var MessageStore: MessageStore = _ 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. * Initializes BroadcastReceiver for discovery, starts discovery and listens for connections.
@ -88,10 +88,10 @@ class ChatService extends Service {
startBluetoothConnections() startBluetoothConnections()
} }
if (!Encrypt.localKeysExist) { if (!Crypto.localKeysExist) {
new Thread(new Runnable { new Thread(new Runnable {
override def run(): Unit = { override def run(): Unit = {
Encrypt.generateLocalKeys() Crypto.generateLocalKeys()
} }
}).start() }).start()
} }
@ -194,9 +194,9 @@ class ChatService extends Service {
if (device.connected) { if (device.connected) {
connections += (device.id -> connections += (device.id ->
new TransferThread(device, socket, this, Encrypt, handleNewMessage)) new TransferThread(device, socket, this, Crypto, handleNewMessage))
connections(device.id).start() 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 { connectionListeners.foreach(l => l.get match {

View file

@ -5,6 +5,7 @@ import java.io._
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.util.Log import android.util.Log
import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMessage} import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message, TextMessage}
import org.msgpack.ScalaMessagePack
/** /**
* Transfers data between connnected devices. * Transfers data between connnected devices.
@ -21,8 +22,6 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
private val Tag: String = "TransferThread" private val Tag: String = "TransferThread"
private val MaxMessageLength = 4096
val InStream: InputStream = val InStream: InputStream =
try { try {
socket.getInputStream socket.getInputStream
@ -46,9 +45,11 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
while (socket.isConnected) { while (socket.isConnected) {
try { try {
val bytes = new Array[Byte](MaxMessageLength) val up = new ScalaMessagePack().createUnpacker(InStream)
InStream.read(bytes) val encrypted = up.readByteArray()
val (message, signature) = Message.read(bytes) val key = up.readByteArray()
val plain = crypto.decrypt(encrypted, key)
val (message, signature) = Message.read(plain)
var messageValid = true var messageValid = true
if (message.sender != device.id) { if (message.sender != device.id) {
@ -97,11 +98,13 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
def send(message: Message): Unit = { def send(message: Message): Unit = {
try { try {
val sig = crypto.calculateSignature(message) val sig = crypto.calculateSignature(message)
val bytes = message.write(sig) val plain = message.write(sig)
OutStream.write(bytes) val (encrypted, key) = crypto.encrypt(message.receiver, plain)
new ScalaMessagePack().createPacker(OutStream)
.write(encrypted)
.write(key)
} catch { } catch {
case e: IOException => case e: IOException => Log.e(Tag, "Failed to write message", e)
Log.e(Tag, "Failed to write message", e)
} }
} }
@ -109,8 +112,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
try { try {
socket.close() socket.close()
} catch { } catch {
case e: IOException => case e: IOException => Log.e(Tag, "Failed to close socket", e);
Log.e(Tag, "Failed to close socket", e);
} }
} }

View file

@ -1,8 +1,10 @@
package com.nutomic.ensichat.messages package com.nutomic.ensichat.messages
import java.io.{File, FileInputStream, FileOutputStream, IOException} import java.io._
import java.security._ import java.security._
import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec} import java.security.spec.{PKCS8EncodedKeySpec, X509EncodedKeySpec}
import javax.crypto.spec.SecretKeySpec
import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
import android.util.Log import android.util.Log
import com.nutomic.ensichat.bluetooth.Device import com.nutomic.ensichat.bluetooth.Device
@ -21,10 +23,20 @@ object Crypto {
val KeySize = 2048 val KeySize = 2048
/** /**
* Name of algorithm used for message signing. * Algorithm used for message signing.
*/ */
val SignAlgorithm = "SHA256withRSA" 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") 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)
}
} }

View file

@ -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 doWrite(packer: Packer) = packer.write(publicKey.getEncoded)
override def equals(a: Any) = 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 override def hashCode = super.hashCode + publicKey.hashCode

View file

@ -97,8 +97,7 @@ abstract class Message(messageType: Int) {
* function and their own data. * function and their own data.
*/ */
override def equals(a: Any): Boolean = a match { override def equals(a: Any): Boolean = a match {
case o: TextMessage => case o: Message => sender == o.sender && receiver == o.receiver && date == o.date
sender == o.sender && receiver == o.receiver && date == o.date
case _ => false case _ => false
} }