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