Added end-to-end message encryption.
This commit is contained in:
parent
6b2ef30888
commit
b4f5569ec9
9 changed files with 131 additions and 31 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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._
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Reference in a new issue