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
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)
}
}

View file

@ -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._

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

View file

@ -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}

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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)
}
}

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 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

View file

@ -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
}