From e4306083114efb14f48379e6a0f9e118807b3879 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 17 Nov 2014 02:58:29 +0200 Subject: [PATCH] Added identicons based on public key when adding new contact. Also fixed a derp with cases. --- .../main/res/layout/dialog_add_contact.xml | 60 ++++++++++++++++++ app/src/main/res/values/strings.xml | 9 +++ .../activities/AddContactsActivity.scala | 57 ++++++++++------- .../ensichat/bluetooth/TransferThread.scala | 2 +- .../nutomic/ensichat/messages/Crypto.scala | 9 +++ .../ensichat/util/IdenticonGenerator.scala | 61 +++++++++++++++++++ 6 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 app/src/main/res/layout/dialog_add_contact.xml create mode 100644 app/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala diff --git a/app/src/main/res/layout/dialog_add_contact.xml b/app/src/main/res/layout/dialog_add_contact.xml new file mode 100644 index 0000000..7d0677f --- /dev/null +++ b/app/src/main/res/layout/dialog_add_contact.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0e3b9e..7b32f61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,15 @@ Do you want to add %1$s as a new contact? + + Your key fingerprint: + + + %1$s\'s key fingerprint: + + + Before accepting, make sure the images match on both devices + No nearby devices found diff --git a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala index 9c1daec..fe9935d 100644 --- a/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala +++ b/app/src/main/scala/com/nutomic/ensichat/activities/AddContactsActivity.scala @@ -3,17 +3,17 @@ package com.nutomic.ensichat.activities import java.util.Date import android.app.AlertDialog -import android.content.DialogInterface import android.content.DialogInterface.OnClickListener +import android.content.{Context, DialogInterface} import android.os.Bundle import android.view._ import android.widget.AdapterView.OnItemClickListener -import android.widget.{AdapterView, ListView, Toast} +import android.widget._ import com.nutomic.ensichat.R import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener import com.nutomic.ensichat.bluetooth.{ChatService, Device} -import com.nutomic.ensichat.messages.{Message, RequestAddContactMessage, ResultAddContactMessage} -import com.nutomic.ensichat.util.DevicesAdapter +import com.nutomic.ensichat.messages.{Crypto, Message, RequestAddContactMessage, ResultAddContactMessage} +import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator} import scala.collection.SortedSet @@ -29,6 +29,8 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection private lazy val Database = service.database + private lazy val Crypto = new Crypto(this.getFilesDir) + /** * Map of devices that should be added. */ @@ -109,8 +111,21 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection } } + val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater] + val view = inflater.inflate(R.layout.dialog_add_contact, null) + + val local = view.findViewById(R.id.local_identicon).asInstanceOf[ImageView] + local.setImageBitmap( + IdenticonGenerator.generate(Crypto.getLocalPublicKey, (150, 150), this)) + val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView] + remoteTitle.setText(getString(R.string.remote_fingerprint_title, device.Name)) + val remote = view.findViewById(R.id.remote_identicon).asInstanceOf[ImageView] + remote.setImageBitmap( + IdenticonGenerator.generate(Crypto.getPublicKey(device.Id), (150, 150), this)) + new AlertDialog.Builder(this) .setTitle(getString(R.string.add_contact_dialog, device.Name)) + .setView(view) .setPositiveButton(android.R.string.yes, onClick) .setNegativeButton(android.R.string.no, onClick) .show() @@ -126,23 +141,23 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection messages.foreach(msg => { if (msg.receiver == service.localDeviceId) { msg match { - case _: ResultAddContactMessage => - // Remote device wants to add us as a contact, show dialog. - val sender = getDevice(msg.sender) - addDeviceDialog(sender) - case m: ResultAddContactMessage => - if (m.Accepted) { - // Remote device accepted us as a contact, update state. - currentlyAdding += (m.sender -> - new AddContactInfo(true, currentlyAdding(m.sender).remoteConfirmed)) - addContactIfBothConfirmed(getDevice(m.sender)) - } else { - // Remote device denied us as a contact, show a toast - // and remove from [[currentlyAdding]]. - Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show() - currentlyAdding -= m.sender - } - case _ => + case _: RequestAddContactMessage => + // Remote device wants to add us as a contact, show dialog. + val sender = getDevice(msg.sender) + addDeviceDialog(sender) + case m: ResultAddContactMessage => + if (m.Accepted) { + // Remote device accepted us as a contact, update state. + currentlyAdding += (m.sender -> + new AddContactInfo(true, currentlyAdding(m.sender).remoteConfirmed)) + addContactIfBothConfirmed(getDevice(m.sender)) + } else { + // Remote device denied us as a contact, show a toast + // and remove from [[currentlyAdding]]. + Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show() + currentlyAdding -= m.sender + } + case _ => } } }) 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 433a1c8..8ed13c2 100644 --- a/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala +++ b/app/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala @@ -4,9 +4,9 @@ import java.io._ import android.bluetooth.BluetoothSocket import android.util.Log +import com.nutomic.ensichat.messages.Message._ import com.nutomic.ensichat.messages.{Crypto, DeviceInfoMessage, Message} import org.msgpack.ScalaMessagePack -import com.nutomic.ensichat.messages.Message._ /** * Transfers data between connnected devices. 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 963e7c9..7963c14 100644 --- a/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala +++ b/app/src/main/scala/com/nutomic/ensichat/messages/Crypto.scala @@ -72,6 +72,15 @@ class Crypto(filesDir: File) { */ def havePublicKey(device: Device.ID): Boolean = new File(keyFolder, device.toString).exists() + /** + * Returns the public key for the given device. + * + * @throws RuntimeException If the key does not exist. + */ + def getPublicKey(device: Device.ID): PublicKey = { + loadKey(device.toString, classOf[PublicKey]) + } + /** * Adds a new public key for a remote device. * diff --git a/app/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala b/app/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala new file mode 100644 index 0000000..48d48b9 --- /dev/null +++ b/app/src/main/scala/com/nutomic/ensichat/util/IdenticonGenerator.scala @@ -0,0 +1,61 @@ +package com.nutomic.ensichat.util + +import java.security.{MessageDigest, PublicKey} + +import android.content.Context +import android.graphics.Bitmap.Config +import android.graphics.{Bitmap, Canvas, Color} + +/** + * Calculates a unique identicon for the given hash. + * + * Based on "Contact Identicons" by David Hamp-Gonsalves (converted from Java to Scala). + * https://github.com/davidhampgonsalves/Contact-Identicons + */ +object IdenticonGenerator { + + val Height: Int = 5 + + val Width: Int = 5 + + /** + * Generates an identicon for the key. + * + * The identicon size is fixed to [[Height]]x[[Width]]. + * + * @param size The size of the bitmap returned. + */ + def generate(key: PublicKey, size: (Int, Int), context: Context): Bitmap = { + // Hash the key. + val digest = MessageDigest.getInstance("SHA-1") + val hash = digest.digest(key.getEncoded) + + // Create base image and colors. + var identicon = Bitmap.createBitmap(Width, Height, Config.ARGB_8888) + val background = Color.parseColor("#f0f0f0") + val r = hash(0) & 255 + val g = hash(1) & 255 + val b = hash(2) & 255 + val foreground = Color.argb(255, r, g, b) + + // Color pixels. + for (x <- 0 until Width) { + val i = if (x < 3) x else 4 - x + var pixelColor: Int = 0 + for (y <- 0 until Height) { + pixelColor = if ((hash(i) >> y & 1) == 1) foreground else background + identicon.setPixel(x, y, pixelColor) + } + } + + // Add border. + val bmpWithBorder = Bitmap.createBitmap(12, 12, identicon.getConfig) + val canvas = new Canvas(bmpWithBorder) + canvas.drawColor(background) + identicon = Bitmap.createScaledBitmap(identicon, 10, 10, false) + canvas.drawBitmap(identicon, 1, 1, null) + + Bitmap.createScaledBitmap(identicon, size._1, size._2, false) + } + +}