Browse Source

Allow adding other device by address, auto request public key

drone-io
Felix Ableitner 5 years ago
parent
commit
f3ec28fef8
  1. 26
      PROTOCOL.md
  2. 2
      android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala
  3. 90
      core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala
  4. 12
      core/src/main/scala/com/nutomic/ensichat/core/messages/Message.scala
  5. 45
      core/src/main/scala/com/nutomic/ensichat/core/messages/body/PublicKeyReply.scala
  6. 50
      core/src/main/scala/com/nutomic/ensichat/core/messages/body/PublicKeyRequest.scala
  7. 3
      core/src/main/scala/com/nutomic/ensichat/core/routing/Router.scala
  8. 2
      core/src/main/scala/com/nutomic/ensichat/core/util/Database.scala
  9. 35
      integration/src/main/scala/com.nutomic.ensichat.integration/Main.scala

26
PROTOCOL.md

@ -282,6 +282,32 @@ Address is the address that is no longer reachable.
SeqNum is the sequence number of the route that is no longer available
(if known). Otherwise, set TargSeqNum = -1. This field is signed.
### PublicKeyRequest (Protocol-Type = 5)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Contains an address for which the sender wants the corresponding public
key.
### PublicKeyReply (Protocol-Type = 6)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Key Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Key (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Contains a node's public key in binary form. Sent in reply to a
PublicKeyRequest.
Content Messages
----------------

2
android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala

@ -127,7 +127,7 @@ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
.setMessage(getString(R.string.dialog_add_contact, user.name))
.setPositiveButton(android.R.string.yes, new OnClickListener {
override def onClick(dialog: DialogInterface, which: Int): Unit = {
database.get.addContact(user)
service.get.addContact(user)
Toast.makeText(ConnectionsActivity.this, R.string.toast_contact_added, Toast.LENGTH_SHORT)
.show()
}

90
core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala

@ -42,12 +42,17 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
private lazy val messageBuffer = new MessageBuffer(crypto.localAddress, requestRoute)
/**
* Messages which we couldn't verify yet because we don't have the sender's public key.
*/
private var unverifiedMessages = Set[Message]()
/**
* Holds all known users.
*
* This is for user names that were received during runtime, and is not persistent.
*/
private var knownUsers = Set[util.User]()
private var knownUsers = Set[User]()
/**
* Generates keys and starts Bluetooth interface.
@ -86,18 +91,30 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
assert(body.contentType != -1)
FutureHelper {
val messageId = settings.get("message_id", 0L)
val header = new ContentHeader(crypto.localAddress, target, seqNumGenerator.next(),
val header = ContentHeader(crypto.localAddress, target, seqNumGenerator.next(),
body.contentType, Some(messageId), Some(DateTime.now), AbstractHeader.InitialForwardingTokens)
settings.put("message_id", messageId + 1)
val msg = new Message(header, body)
val encrypted = crypto.encryptAndSign(msg)
router.forwardMessage(encrypted)
forwardMessageToRelays(encrypted)
onNewMessage(msg)
if (crypto.havePublicKey(target)) {
val encrypted = crypto.encryptAndSign(msg)
router.forwardMessage(encrypted)
forwardMessageToRelays(encrypted)
}
else {
logger.info(s"Public key missing for $target, buffering message and sending key request")
requestPublicKey(target)
}
}
}
private def requestPublicKey(address: Address): Unit = {
val header = MessageHeader(PublicKeyRequest.Type, crypto.localAddress, Address.Broadcast, seqNumGenerator.next(), 0)
val msg = new Message(header, PublicKeyRequest(address))
router.forwardMessage(crypto.sign(msg))
}
private def requestRoute(target: Address): Unit = {
assert(localRoutesInfo.getRoute(target).isEmpty)
val seqNum = seqNumGenerator.next()
@ -205,6 +222,38 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
.foreach(routeError(_, None))
}
}
return
case pkr: PublicKeyRequest =>
if (crypto.havePublicKey(pkr.address)) {
val header = MessageHeader(PublicKeyReply.Type, crypto.localAddress, msg.header.origin, seqNumGenerator.next(), 0)
val msg2 = new Message(header, PublicKeyReply(crypto.getPublicKey(pkr.address)))
router.forwardMessage(crypto.sign(msg2), Option(previousHop))
}
else {
router.forwardMessage(msg)
}
return
case pkr: PublicKeyReply =>
if (msg.header.target != crypto.localAddress) {
router.forwardMessage(msg)
return
}
val address = crypto.calculateAddress(pkr.key)
if (crypto.havePublicKey(address))
return
logger.info(s"Received public key for $address, resending and decrypting messages")
crypto.addPublicKey(address, pkr.key)
database.getMessages(address)
.filter(_.header.target == address)
.foreach{ m =>
sendTo(address, m.body)
}
val current = unverifiedMessages
.filter(_.header.origin == address)
current.foreach(decryptMessage)
unverifiedMessages --= current
return
case _ =>
}
@ -214,6 +263,17 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
return
}
if (!crypto.havePublicKey(msg.header.origin)) {
logger.info(s"Received message from ${msg.header.origin} but don't have public key, buffering")
unverifiedMessages += msg
requestPublicKey(msg.header.origin)
return
}
decryptMessage(msg)
}
private def decryptMessage(msg: Message): Unit = {
val plainMsg =
try {
if (!crypto.verify(msg)) {
@ -241,7 +301,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
if (plainMsg.body.contentType == Text.Type) {
logger.trace(s"Sending confirmation for $plainMsg")
sendTo(plainMsg.header.origin, new messages.body.MessageReceived(plainMsg.header.messageId.get))
sendTo(plainMsg.header.origin, new MessageReceived(plainMsg.header.messageId.get))
}
onNewMessage(plainMsg)
@ -280,13 +340,13 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
*/
private def onNewMessage(msg: Message): Unit = msg.body match {
case ui: UserInfo =>
val contact = new util.User(msg.header.origin, ui.name, ui.status)
val contact = User(msg.header.origin, ui.name, ui.status)
knownUsers += contact
if (database.getContact(msg.header.origin).nonEmpty)
database.updateContact(contact)
callbacks.onConnectionsChanged()
case mr: messages.body.MessageReceived =>
case mr: MessageReceived =>
database.setMessageConfirmed(mr.messageId)
case _ =>
val origin = msg.header.origin
@ -339,8 +399,8 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
else
logger.info("Node " + sender + " connected")
sendTo(sender, new UserInfo(settings.get(SettingsInterface.KeyUserName, ""),
settings.get(SettingsInterface.KeyUserStatus, "")))
sendTo(sender, UserInfo(settings.get(SettingsInterface.KeyUserName, ""),
settings.get(SettingsInterface.KeyUserStatus, "")))
callbacks.onConnectionsChanged()
resendMissingRouteMessages()
messageBuffer.getAllMessages
@ -372,7 +432,7 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
def getUser(address: Address) =
allKnownUsers()
.find(_.address == address)
.getOrElse(new util.User(address, address.toString(), ""))
.getOrElse(User(address, address.toString(), ""))
/**
* This method should be called when the local device's internet connection has changed in any way.
@ -382,4 +442,12 @@ final class ConnectionHandler(settings: SettingsInterface, database: Database,
.find(_.isInstanceOf[InternetInterface])
.foreach(_.asInstanceOf[InternetInterface].connectionChanged())
}
def addContact(user: User): Unit = {
database.addContact(user)
if (!crypto.havePublicKey(user.address)) {
requestPublicKey(user.address)
}
}
}

12
core/src/main/scala/com/nutomic/ensichat/core/messages/Message.scala

@ -50,11 +50,13 @@ object Message {
val body =
header.protocolType match {
case messages.body.ConnectionInfo.Type => messages.body.ConnectionInfo.read(remaining)
case RouteRequest.Type => RouteRequest.read(remaining)
case RouteReply.Type => RouteReply.read(remaining)
case RouteError.Type => RouteError.read(remaining)
case _ => new EncryptedBody(remaining)
case ConnectionInfo.Type => ConnectionInfo.read(remaining)
case RouteRequest.Type => RouteRequest.read(remaining)
case RouteReply.Type => RouteReply.read(remaining)
case RouteError.Type => RouteError.read(remaining)
case PublicKeyRequest.Type => PublicKeyRequest.read(remaining)
case PublicKeyReply.Type => PublicKeyReply.read(remaining)
case _ => EncryptedBody(remaining)
}
new Message(header, crypto, body)

45
core/src/main/scala/com/nutomic/ensichat/core/messages/body/PublicKeyReply.scala

@ -0,0 +1,45 @@
package com.nutomic.ensichat.core.messages.body
import java.nio.ByteBuffer
import java.security.spec.X509EncodedKeySpec
import java.security.{KeyFactory, PublicKey}
import com.nutomic.ensichat.core.util.BufferUtils
import com.nutomic.ensichat.core.util.Crypto
object PublicKeyReply {
val Type = 6
/**
* Constructs [[ConnectionInfo]] instance from byte array.
*/
def read(array: Array[Byte]): PublicKeyReply = {
val b = ByteBuffer.wrap(array)
val length = BufferUtils.getUnsignedInt(b).toInt
val encoded = new Array[Byte](length)
b.get(encoded, 0, length)
val factory = KeyFactory.getInstance(Crypto.PublicKeyAlgorithm)
val key = factory.generatePublic(new X509EncodedKeySpec(encoded))
new PublicKeyReply(key)
}
}
case class PublicKeyReply(key: PublicKey) extends MessageBody {
override def protocolType = PublicKeyRequest.Type
override def contentType = -1
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
BufferUtils.putUnsignedInt(b, key.getEncoded.length)
b.put(key.getEncoded)
b.array()
}
override def length = 4 + key.getEncoded.length
}

50
core/src/main/scala/com/nutomic/ensichat/core/messages/body/PublicKeyRequest.scala

@ -0,0 +1,50 @@
package com.nutomic.ensichat.core.messages.body
import java.nio.ByteBuffer
import com.nutomic.ensichat.core.routing.Address
import com.nutomic.ensichat.core.util.BufferUtils
object PublicKeyRequest {
val Type = 5
/**
* Constructs [[Text]] instance from byte array.
*/
def read(array: Array[Byte]): PublicKeyRequest = {
val b = ByteBuffer.wrap(array)
val length = BufferUtils.getUnsignedInt(b).toInt
val bytes = new Array[Byte](length)
b.get(bytes, 0, length)
new PublicKeyRequest(new Address(bytes))
}
}
case class PublicKeyRequest(address: Address) extends MessageBody {
require(address != Address.Broadcast, "")
require(address != Address.Null, "")
override def protocolType = PublicKeyRequest.Type
override def contentType = -1
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
val bytes = address.bytes
BufferUtils.putUnsignedInt(b, bytes.length)
b.put(bytes)
b.array()
}
override def equals(a: Any): Boolean = a match {
case o: PublicKeyRequest => address == o.address
case _ => false
}
override def length = 4 + address.bytes.length
}

3
core/src/main/scala/com/nutomic/ensichat/core/routing/Router.scala

@ -67,7 +67,8 @@ private[core] class Router(routesInfo: LocalRoutesInfo, send: (Address, Message)
send(a, incHopCount(msg))
markMessageSeen((msg.header.origin, msg.header.seqNum))
case None =>
noRouteFound(msg)
if (msg.header.isInstanceOf[ContentHeader])
noRouteFound(msg)
}
}

2
core/src/main/scala/com/nutomic/ensichat/core/util/Database.scala

@ -160,7 +160,7 @@ class Database(path: File, settings: SettingsInterface, callbackInterface: Callb
/**
* Inserts the user as a new contact.
*/
def addContact(contact: User): Unit = {
private[core] def addContact(contact: User): Unit = {
Await.result(db.run(contacts += contact), Duration.Inf)
callbackInterface.onContactsUpdated()
}

35
integration/src/main/scala/com.nutomic.ensichat.integration/Main.scala

@ -22,6 +22,7 @@ import scalax.file.Path
*/
object Main extends App {
/*
testNeighborSending()
testMeshMessageSending()
testIndirectRelay()
@ -30,6 +31,8 @@ object Main extends App {
testSendDelayed()
testRouteChange()
testMessageConfirmation()
*/
testKeyRequest()
private def testNeighborSending(): Unit = {
val node1 = Await.result(createNode(1), Duration.Inf)
@ -169,6 +172,38 @@ object Main extends App {
nodes.foreach(_.stop())
}
private def testKeyRequest(): Unit = {
val nodes = createNodes(4)
connectNodes(nodes(0), nodes(1))
connectNodes(nodes(1), nodes(2))
connectNodes(nodes(2), nodes(3))
val origin = nodes(0)
val target = nodes(3)
System.out.println(s"sendMessage(${origin.index}, ${target.index})")
val text = s"${origin.index} to ${target.index}"
origin.connectionHandler.sendTo(target.crypto.localAddress, new Text(text))
val latch = new CountDownLatch(1)
Future {
val exists =
target.eventQueue.toStream.exists { event =>
if (event._1 != LocalNode.EventType.MessageReceived)
false
else {
event._2.get.body match {
case t: Text => t.text == text
case _ => false
}
}
}
assert(exists, s"message from ${origin.index} did not arrive at ${target.index}")
latch.countDown()
}
assert(latch.await(3, TimeUnit.SECONDS))
}
private def createNodes(count: Int): Seq[LocalNode] = {
val nodes = Await.result(Future.sequence((0 until count).map(createNode)), Duration.Inf)
nodes.foreach(n => System.out.println(s"Node ${n.index} has address ${n.crypto.localAddress}"))