Added server project for internet routing.

Also adjusted Log trait and visibility of library classes.
This commit is contained in:
Felix Ableitner 2015-10-18 22:49:26 +02:00
parent 28cb4f15d9
commit 199b185861
49 changed files with 671 additions and 121 deletions

View file

@ -37,8 +37,8 @@ android {
defaultConfig {
applicationId "com.nutomic.ensichat"
targetSdkVersion 23
versionCode 8
versionName "0.1.7"
versionCode project.properties.versionCode.toInteger()
versionName project.properties.versionName
multiDexEnabled true
testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
}

View file

@ -6,12 +6,14 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<application
android:name="android.support.multidex.MultiDexApplication"
android:name=".App"
android:allowBackup="true"
android:fullBackupContent="true"
android:icon="@drawable/ic_launcher"
@ -60,6 +62,12 @@
<service android:name=".service.ChatService" />
<receiver android:name=".util.NetworkChangedReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,15 @@
package com.nutomic.ensichat
import android.support.multidex.MultiDexApplication
import com.nutomic.ensichat.core.interfaces.Log
import com.nutomic.ensichat.util.{Logging, PRNGFixes}
class App extends MultiDexApplication {
override def onCreate(): Unit = {
super.onCreate()
Log.setLogInstance(new Logging())
PRNGFixes.apply()
}
}

View file

@ -19,8 +19,6 @@ import com.nutomic.ensichat.views.UsersAdapter
*/
class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
private val Tag = "AddContactsActivity"
private lazy val database = new Database(this)
private lazy val adapter = new UsersAdapter(this)

View file

@ -11,8 +11,8 @@ import android.view.{KeyEvent, View}
import android.widget.TextView.OnEditorActionListener
import android.widget.{Button, EditText, TextView}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.core.interfaces.Settings
import com.nutomic.ensichat.core.interfaces.Settings._
import com.nutomic.ensichat.core.interfaces.SettingsInterface
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
/**
* Shown on first start, lets the user enter their name.
@ -69,11 +69,11 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
preferences
.edit()
.putBoolean(KeyIsFirstStart, false)
.putString(Settings.KeyUserName, username.getText.toString.trim)
.putString(Settings.KeyUserStatus, Settings.DefaultUserStatus)
.putBoolean(Settings.KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
.putString(Settings.KeyScanInterval, DefaultScanInterval.toString)
.putString(Settings.KeyMaxConnections, DefaultMaxConnections.toString)
.putString(SettingsInterface.KeyUserName, username.getText.toString.trim)
.putString(SettingsInterface.KeyUserStatus, SettingsInterface.DefaultUserStatus)
.putBoolean(SettingsInterface.KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
.putString(SettingsInterface.KeyScanInterval, DefaultScanInterval.toString)
.putString(SettingsInterface.KeyMaxConnections, DefaultMaxConnections.toString)
.apply()
startMainActivity()

View file

@ -8,7 +8,7 @@ import android.util.Log
/**
* Attempts to connect to another device and calls [[onConnected]] on success.
*/
class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
class BluetoothConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
private val Tag = "ConnectThread"

View file

@ -9,7 +9,7 @@ import android.preference.PreferenceManager
import android.util.Log
import com.nutomic.ensichat.R
import com.nutomic.ensichat.core.body.ConnectionInfo
import com.nutomic.ensichat.core.interfaces.{Settings, TransmissionInterface}
import com.nutomic.ensichat.core.interfaces.{SettingsInterface, TransmissionInterface}
import com.nutomic.ensichat.core.{Address, ConnectionHandler, Message}
import com.nutomic.ensichat.service.ChatService
@ -38,9 +38,9 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
private var devices = new HashMap[Device.ID, Device]()
private var connections = new HashMap[Device.ID, TransferThread]()
private var connections = new HashMap[Device.ID, BluetoothTransferThread]()
private var listenThread: Option[ListenThread] = None
private var listenThread: Option[BluetoothListenThread] = None
private var cancelDiscovery = false
@ -86,7 +86,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
* Starts discovery and listening.
*/
private def startBluetoothConnections(): Unit = {
listenThread = Some(new ListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened))
listenThread = Some(new BluetoothListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened))
listenThread.get.start()
cancelDiscovery = false
discover()
@ -106,7 +106,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
val pm = PreferenceManager.getDefaultSharedPreferences(context)
val scanInterval =
pm.getString(Settings.KeyScanInterval, Settings.DefaultScanInterval.toString).toInt * 1000
pm.getString(SettingsInterface.KeyScanInterval, SettingsInterface.DefaultScanInterval.toString).toInt * 1000
mainHandler.postDelayed(new Runnable {
override def run(): Unit = discover()
}, scanInterval)
@ -128,7 +128,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
override def onReceive(context: Context, intent: Intent): Unit = {
discovered.filterNot(d => connections.keySet.contains(d.id))
.foreach { d =>
new ConnectThread(d, connectionOpened).start()
new BluetoothConnectThread(d, connectionOpened).start()
devices += (d.id -> d)
}
discovered = Set[Device]()
@ -162,7 +162,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
def connectionOpened(device: Device, socket: BluetoothSocket): Unit = {
devices += (device.id -> device)
connections += (device.id ->
new TransferThread(context, device, socket, this, crypto, onReceiveMessage))
new BluetoothTransferThread(context, device, socket, this, crypto, onReceiveMessage))
connections(device.id).start()
}
@ -198,13 +198,18 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
/**
* Sends the message to nextHop.
*/
override def send(nextHop: Address, msg: Message): Unit =
connections.get(addressDeviceMap(nextHop)).foreach(_.send(msg))
override def send(nextHop: Address, msg: Message): Unit = {
addressDeviceMap
.find(_._1 == nextHop)
.map(i => connections.get(i._2))
.getOrElse(None)
.foreach(_.send(msg))
}
/**
* Returns all active Bluetooth connections.
*/
def getConnections: Set[Address] =
override def getConnections: Set[Address] =
connections.map(x => addressDeviceMap.find(_._2 == x._1).get._1).toSet
}

View file

@ -10,7 +10,7 @@ import android.util.Log
*
* @param name Service name to broadcast.
*/
class ListenThread(name: String, adapter: BluetoothAdapter,
class BluetoothListenThread(name: String, adapter: BluetoothAdapter,
onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
private val Tag = "ListenThread"

View file

@ -17,7 +17,7 @@ import com.nutomic.ensichat.core.{Address, Crypto, Message}
* @param socket An open socket to the given device.
* @param onReceive Called when a message was received from the other device.
*/
class TransferThread(context: Context, device: Device, socket: BluetoothSocket, handler: BluetoothInterface,
class BluetoothTransferThread(context: Context, device: Device, socket: BluetoothSocket, handler: BluetoothInterface,
crypto: Crypto, onReceive: (Message, Device.ID) => Unit) extends Thread {
private val Tag = "TransferThread"
@ -30,6 +30,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
close()
null
}
@ -39,6 +40,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
close()
null
}

View file

@ -15,7 +15,7 @@ import android.view._
import android.widget.{ListView, TextView}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity}
import com.nutomic.ensichat.core.interfaces.Settings
import com.nutomic.ensichat.core.interfaces.SettingsInterface
import com.nutomic.ensichat.service.{CallbackHandler, ChatService}
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.UsersAdapter
@ -102,7 +102,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
bundle.putString(
IdenticonFragment.ExtraAddress, ChatService.newCrypto(getActivity).localAddress.toString)
bundle.putString(
IdenticonFragment.ExtraUserName, prefs.getString(Settings.KeyUserName, ""))
IdenticonFragment.ExtraUserName, prefs.getString(SettingsInterface.KeyUserName, ""))
fragment.setArguments(bundle)
fragment.show(getFragmentManager, "dialog")
true

View file

@ -3,14 +3,13 @@ package com.nutomic.ensichat.fragments
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle
import android.preference.Preference.OnPreferenceChangeListener
import android.preference.{Preference, PreferenceFragment, PreferenceManager}
import com.nutomic.ensichat.{BuildConfig, R}
import android.preference.{PreferenceFragment, PreferenceManager}
import com.nutomic.ensichat.activities.EnsichatActivity
import com.nutomic.ensichat.core.body.UserInfo
import com.nutomic.ensichat.core.interfaces.Settings._
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
import com.nutomic.ensichat.fragments.SettingsFragment._
import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.{BuildConfig, R}
object SettingsFragment {
val Version = "version"

View file

@ -2,8 +2,8 @@ package com.nutomic.ensichat.service
import android.content.{Context, Intent}
import android.support.v4.content.LocalBroadcastManager
import com.nutomic.ensichat.core.{ConnectionHandler, Message}
import com.nutomic.ensichat.core.interfaces.CallbackInterface
import com.nutomic.ensichat.core.{ConnectionHandler, Message}
import com.nutomic.ensichat.service.CallbackHandler._
object CallbackHandler {

View file

@ -6,9 +6,8 @@ import android.app.Service
import android.content.{Context, Intent}
import android.os.Handler
import com.nutomic.ensichat.bluetooth.BluetoothInterface
import com.nutomic.ensichat.core.interfaces.Log
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto}
import com.nutomic.ensichat.util.{Database, PRNGFixes, SettingsWrapper}
import com.nutomic.ensichat.util.{Database, SettingsWrapper}
object ChatService {
@ -17,6 +16,8 @@ object ChatService {
private def keyFolder(context: Context) = new File(context.getFilesDir, "keys")
def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context))
val ActionNetworkChanged = "network_changed"
}
class ChatService extends Service {
@ -29,23 +30,26 @@ class ChatService extends Service {
private lazy val connectionHandler =
new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler,
ChatService.keyFolder(this))
ChatService.newCrypto(this))
override def onBind(intent: Intent) = binder
override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
if (intent.getAction == ChatService.ActionNetworkChanged)
connectionHandler.internetConnectionChanged()
Service.START_STICKY
}
/**
* Generates keys and starts Bluetooth interface.
*/
override def onCreate(): Unit = {
super.onCreate()
PRNGFixes.apply()
Log.setLogClass(classOf[android.util.Log])
notificationHandler.showPersistentNotification()
connectionHandler.addTransmissionInterface(new BluetoothInterface(this, new Handler(),
connectionHandler))
connectionHandler.start()
connectionHandler.setTransmissionInterface(new BluetoothInterface(this, new Handler(),
connectionHandler))
}
override def onDestroy(): Unit = {

View file

@ -8,7 +8,7 @@ import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.MainActivity
import com.nutomic.ensichat.core.Message
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.interfaces.Settings
import com.nutomic.ensichat.core.interfaces.SettingsInterface
import com.nutomic.ensichat.service.NotificationHandler._
object NotificationHandler {
@ -65,7 +65,7 @@ class NotificationHandler(context: Context) {
*/
private def defaults(): Int = {
val sp = PreferenceManager.getDefaultSharedPreferences(context)
if (sp.getBoolean(Settings.KeyNotificationSoundsOn, Settings.DefaultNotificationSoundsOn))
if (sp.getBoolean(SettingsInterface.KeyNotificationSoundsOn, SettingsInterface.DefaultNotificationSoundsOn))
Notification.DEFAULT_ALL
else
Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS

View file

@ -0,0 +1,13 @@
package com.nutomic.ensichat.util
import android.util
import com.nutomic.ensichat.core.interfaces.Log
class Logging extends Log {
def v(tag: String, message: String, tr: Throwable = null) = util.Log.v(tag, message, tr)
def d(tag: String, message: String, tr: Throwable = null) = util.Log.d(tag, message, tr)
def i(tag: String, message: String, tr: Throwable = null) = util.Log.i(tag, message, tr)
def w(tag: String, message: String, tr: Throwable = null) = util.Log.w(tag, message, tr)
def e(tag: String, message: String, tr: Throwable = null) = util.Log.e(tag, message, tr)
}

View file

@ -0,0 +1,15 @@
package com.nutomic.ensichat.util
import android.content.{Intent, Context, BroadcastReceiver}
import android.net.ConnectivityManager
import com.nutomic.ensichat.service.ChatService
class NetworkChangedReceiver extends BroadcastReceiver {
override def onReceive(context: Context, intent: Intent): Unit = {
val intent = new Intent(context, classOf[ChatService])
intent.setAction(ChatService.ActionNetworkChanged)
context.startService(intent)
}
}

View file

@ -2,9 +2,9 @@ package com.nutomic.ensichat.util
import android.content.Context
import android.preference.PreferenceManager
import com.nutomic.ensichat.core.interfaces.Settings
import com.nutomic.ensichat.core.interfaces.SettingsInterface
class SettingsWrapper(context: Context) extends Settings {
class SettingsWrapper(context: Context) extends SettingsInterface {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)

View file

@ -24,7 +24,7 @@ object Address {
*
* @param bytes SHA-256 hash of the node's public key.
*/
case class Address(bytes: Array[Byte]) {
final case class Address(bytes: Array[Byte]) {
require(bytes.length == Address.Length, "Invalid address length (was " + bytes.length + ")")

View file

@ -6,6 +6,7 @@ import java.util.Date
import com.nutomic.ensichat.core.body.{ConnectionInfo, MessageBody, UserInfo}
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces._
import com.nutomic.ensichat.core.internet.InternetInterface
import com.nutomic.ensichat.core.util.FutureHelper
import scala.concurrent.ExecutionContext.Implicits.global
@ -13,14 +14,12 @@ import scala.concurrent.ExecutionContext.Implicits.global
/**
* High-level handling of all message transfers and callbacks.
*/
class ConnectionHandler(settings: Settings, database: DatabaseInterface,
callbacks: CallbackInterface, keyFolder: File) {
final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInterface,
callbacks: CallbackInterface, crypto: Crypto) {
private val Tag = "ConnectionHandler"
private lazy val crypto = new Crypto(settings, keyFolder)
private var transmissionInterface: TransmissionInterface = _
private var transmissionInterfaces = Set[TransmissionInterface]()
private lazy val router = new Router(connections, sendVia)
@ -40,16 +39,20 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
FutureHelper {
crypto.generateLocalKeys()
Log.i(Tag, "Service started, address is " + crypto.localAddress)
transmissionInterfaces += new InternetInterface(this, crypto)
transmissionInterfaces.foreach(_.create())
}
}
def stop(): Unit = {
transmissionInterface.destroy()
transmissionInterfaces.foreach(_.destroy())
}
def setTransmissionInterface(interface: TransmissionInterface) = {
transmissionInterface = interface
transmissionInterface.create()
/**
* NOTE: This *must* be called before [[start()]], or it will have no effect.
*/
def addTransmissionInterface(interface: TransmissionInterface) = {
transmissionInterfaces += interface
}
/**
@ -70,7 +73,7 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
}
private def sendVia(nextHop: Address, msg: Message) =
transmissionInterface.send(nextHop, msg)
transmissionInterfaces.foreach(_.send(nextHop, msg))
/**
* Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]].
@ -78,10 +81,8 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
def onMessageReceived(msg: Message): Unit = {
if (msg.header.target == crypto.localAddress) {
crypto.verifyAndDecrypt(msg) match {
case Some(msg) => onNewMessage(msg)
case None =>
Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
return
case Some(m) => onNewMessage(m)
case None => Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
}
} else {
router.onReceive(msg)
@ -118,7 +119,8 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
* @return True if the connection is valid
*/
def onConnectionOpened(msg: Message): Boolean = {
val maxConnections = settings.get(Settings.KeyMaxConnections, Settings.DefaultMaxConnections.toString).toInt
val maxConnections = settings.get(SettingsInterface.KeyMaxConnections,
SettingsInterface.DefaultMaxConnections.toString).toInt
if (connections().size == maxConnections) {
Log.i(Tag, "Maximum number of connections reached")
return false
@ -144,17 +146,22 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
}
Log.i(Tag, "Node " + sender + " connected")
sendTo(sender, new UserInfo(settings.get(Settings.KeyUserName, ""),
settings.get(Settings.KeyUserStatus, "")))
sendTo(sender, new UserInfo(settings.get(SettingsInterface.KeyUserName, ""),
settings.get(SettingsInterface.KeyUserStatus, "")))
callbacks.onConnectionsChanged()
true
}
def onConnectionClosed() = callbacks.onConnectionsChanged()
def connections() = transmissionInterface.getConnections
def connections(): Set[Address] = transmissionInterfaces.flatMap(_.getConnections)
def getUser(address: Address) =
knownUsers.find(_.address == address).getOrElse(new User(address, address.toString, ""))
def internetConnectionChanged(): Unit = {
transmissionInterfaces
.find(_.isInstanceOf[InternetInterface])
.foreach(_.asInstanceOf[InternetInterface].connectionChanged())
}
}

View file

@ -9,7 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
import com.nutomic.ensichat.core.Crypto._
import com.nutomic.ensichat.core.body._
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces.{Log, Settings}
import com.nutomic.ensichat.core.interfaces.{Log, SettingsInterface}
object Crypto {
@ -20,6 +20,11 @@ object Crypto {
*/
val PublicKeyAlgorithm = "RSA"
/**
* Algorithm used to read public keys.
*/
val CipherAlgorithm = "RSA/ECB/PKCS1Padding"
/**
* Length of the local public/private keypair in bits.
*/
@ -50,7 +55,7 @@ object Crypto {
/**
* Name of the preference where the local address is stored.
*/
private val LocalAddressKey = "local_address"
val LocalAddressKey = "local_address"
/**
* Filename of the local private key in [[Crypto.keyFolder]].
@ -60,7 +65,7 @@ object Crypto {
/**
* Filename of the local public key in [[Crypto.keyFolder]].
*/
private val PublicKeyAlias = "local-public"
val PublicKeyAlias = "local-public"
}
@ -69,17 +74,17 @@ object Crypto {
*
* @param keyFolder Folder where private and public keys are stored.
*/
class Crypto(settings: Settings, keyFolder: File) {
class Crypto(settings: SettingsInterface, keyFolder: File) {
private val Tag = "Crypto"
/**
* Generates a new key pair using [[ [[Crypto.keyFolder]].]] with [[KeySize]] bits and stores the
* Generates a new key pair using [[keyFolder]] with [[PublicKeySize]] bits and stores the
* keys.
*
* Does nothing if the key pair already exists.
*/
def generateLocalKeys(): Unit = {
private[core] def generateLocalKeys(): Unit = {
if (localKeysExist)
return
@ -92,7 +97,7 @@ class Crypto(settings: Settings, keyFolder: File) {
address = calculateAddress(keyPair.getPublic)
// The hash must have at least one bit set to not collide with the broadcast address.
// Never generate an invalid address.
} while(address == Address.Broadcast || address == Address.Null)
settings.put(LocalAddressKey, address.toString)
@ -105,7 +110,7 @@ class Crypto(settings: Settings, keyFolder: File) {
/**
* Returns true if we have a public key stored for the given device.
*/
def havePublicKey(address: Address): Boolean = new File(keyFolder, address.toString).exists()
private[core] def havePublicKey(address: Address) = new File(keyFolder, address.toString).exists()
/**
* Returns the public key for the given device.
@ -113,7 +118,7 @@ class Crypto(settings: Settings, keyFolder: File) {
* @throws RuntimeException If the key does not exist.
*/
@throws[RuntimeException]
def getPublicKey(address: Address): PublicKey = {
private[core] def getPublicKey(address: Address): PublicKey = {
loadKey(address.toString, classOf[PublicKey])
}
@ -123,7 +128,7 @@ class Crypto(settings: Settings, keyFolder: File) {
* @throws RuntimeException If a key already exists for this address.
*/
@throws[RuntimeException]
def addPublicKey(address: Address, key: PublicKey): Unit = {
private[core] def addPublicKey(address: Address, key: PublicKey): Unit = {
if (havePublicKey(address))
throw new RuntimeException("Already have key for " + address + ", not overwriting")
@ -138,7 +143,7 @@ class Crypto(settings: Settings, keyFolder: File) {
new Message(msg.header, new CryptoData(Option(sig.sign), msg.crypto.key), msg.body)
}
def verify(msg: Message, key: Option[PublicKey] = None): Boolean = {
private[core] def verify(msg: Message, key: Option[PublicKey] = None): Boolean = {
val sig = Signature.getInstance(SigningAlgorithm)
lazy val defaultKey = loadKey(msg.header.origin.toString, classOf[PublicKey])
sig.initVerify(key.getOrElse(defaultKey))
@ -149,7 +154,7 @@ class Crypto(settings: Settings, keyFolder: File) {
/**
* Returns true if the local private and public key exist.
*/
def localKeysExist = new File(keyFolder, PublicKeyAlias).exists()
private[core] def localKeysExist = new File(keyFolder, PublicKeyAlias).exists()
/**
* Returns the local public key.
@ -193,7 +198,7 @@ class Crypto(settings: Settings, keyFolder: File) {
* @return The key read from storage.
* @throws RuntimeException If the key does not exist.
*/
private def loadKey[T](alias: String, keyType: Class[T]): T = {
private[core] def loadKey[T](alias: String, keyType: Class[T]): T = {
val path = new File(keyFolder, alias)
if (!path.exists()) {
throw new RuntimeException("The requested key with alias " + alias + " does not exist")
@ -221,11 +226,11 @@ class Crypto(settings: Settings, keyFolder: File) {
}
}
def encryptAndSign(msg: Message, key: Option[PublicKey] = None): Message = {
private[core] def encryptAndSign(msg: Message, key: Option[PublicKey] = None): Message = {
sign(encrypt(msg, key))
}
def verifyAndDecrypt(msg: Message, key: Option[PublicKey] = None): Option[Message] = {
private[core] def verifyAndDecrypt(msg: Message, key: Option[PublicKey] = None): Option[Message] = {
if (verify(msg, key))
Option(decrypt(msg))
else
@ -240,7 +245,7 @@ class Crypto(settings: Settings, keyFolder: File) {
val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.body.write))
// Asymmetric encryption of secret key
val asymmetricCipher = Cipher.getInstance(PublicKeyAlgorithm)
val asymmetricCipher = Cipher.getInstance(CipherAlgorithm)
lazy val defaultKey = loadKey(msg.header.target.toString, classOf[PublicKey])
asymmetricCipher.init(Cipher.WRAP_MODE, key.getOrElse(defaultKey))
@ -250,7 +255,7 @@ class Crypto(settings: Settings, keyFolder: File) {
private def decrypt(msg: Message): Message = {
// Asymmetric decryption of secret key
val asymmetricCipher = Cipher.getInstance(PublicKeyAlgorithm)
val asymmetricCipher = Cipher.getInstance(CipherAlgorithm)
asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey]))
val key = asymmetricCipher.unwrap(msg.crypto.key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY)

View file

@ -5,7 +5,7 @@ import com.nutomic.ensichat.core.header.{ContentHeader, MessageHeader}
/**
* Forwards messages to all connected devices.
*/
class Router(activeConnections: () => Set[Address], send: (Address, Message) => Unit) {
final private[core] class Router(activeConnections: () => Set[Address], send: (Address, Message) => Unit) {
private var messageSeen = Set[(Address, Int)]()

View file

@ -1,12 +1,12 @@
package com.nutomic.ensichat.core
import com.nutomic.ensichat.core.header.ContentHeader
import com.nutomic.ensichat.core.interfaces.Settings
import com.nutomic.ensichat.core.interfaces.SettingsInterface
/**
* Generates sequence numbers according to protocol, which are stored persistently.
*/
class SeqNumGenerator(preferences: Settings) {
final private[core] class SeqNumGenerator(preferences: SettingsInterface) {
private val KeySequenceNumber = "sequence_number"

View file

@ -1,3 +1,3 @@
package com.nutomic.ensichat.core
case class User(address: Address, name: String, status: String)
final case class User(address: Address, name: String, status: String)

View file

@ -32,7 +32,7 @@ object ConnectionInfo {
/**
* Holds a node's public key.
*/
case class ConnectionInfo(key: PublicKey) extends MessageBody {
final case class ConnectionInfo(key: PublicKey) extends MessageBody {
override def protocolType = ConnectionInfo.Type

View file

@ -35,7 +35,7 @@ object CryptoData {
/**
* Holds the signature and (optional) key that are stored in a message.
*/
case class CryptoData(signature: Option[Array[Byte]], key: Option[Array[Byte]]) {
final case class CryptoData(signature: Option[Array[Byte]], key: Option[Array[Byte]]) {
override def equals(a: Any): Boolean = a match {
case o: CryptoData => util.Arrays.equals(signature.orNull, o.signature.orNull) &&

View file

@ -3,7 +3,7 @@ package com.nutomic.ensichat.core.body
/**
* Represents the data in an encrypted message body.
*/
case class EncryptedBody(data: Array[Byte]) extends MessageBody {
final case class EncryptedBody(data: Array[Byte]) extends MessageBody {
override def protocolType = -1

View file

@ -25,7 +25,7 @@ object Text {
/**
* Holds a plain text message.
*/
case class Text(text: String) extends MessageBody {
final case class Text(text: String) extends MessageBody {
override def protocolType = -1

View file

@ -29,7 +29,7 @@ object UserInfo {
/**
* Holds display name and status of the sender.
*/
case class UserInfo(name: String, status: String) extends MessageBody {
final case class UserInfo(name: String, status: String) extends MessageBody {
override def protocolType = -1

View file

@ -39,7 +39,7 @@ object ContentHeader {
*
* This is [[AbstractHeader]] with messageId and time fields set.
*/
case class ContentHeader(override val origin: Address,
final case class ContentHeader(override val origin: Address,
override val target: Address,
override val seqNum: Int,
contentType: Int,

View file

@ -41,7 +41,7 @@ object MessageHeader {
*
* This is the same as [[AbstractHeader]].
*/
case class MessageHeader(override val protocolType: Int,
final case class MessageHeader(override val protocolType: Int,
override val origin: Address,
override val target: Address,
override val seqNum: Int,

View file

@ -7,4 +7,5 @@ trait CallbackInterface {
def onMessageReceived(msg: Message): Unit
def onConnectionsChanged(): Unit
}

View file

@ -2,28 +2,24 @@ package com.nutomic.ensichat.core.interfaces
object Log {
def setLogClass[T](logClass: Class[T]) = {
this.logClass = Option(logClass)
}
def setLogInstance(log: Log) = instance = Option(log)
private var logClass: Option[Class[_]] = None
private var instance: Option[Log] = None
def v(tag: String, message: String, tr: Throwable = null) = log("v", tag, message, tr)
def d(tag: String, message: String, tr: Throwable = null) = log("d", tag, message, tr)
def i(tag: String, message: String, tr: Throwable = null) = log("i", tag, message, tr)
def w(tag: String, message: String, tr: Throwable = null) = log("w", tag, message, tr)
def e(tag: String, message: String, tr: Throwable = null) = log("e", tag, message, tr)
private def log(level: String, tag: String, message: String, throwable: Throwable) = logClass match {
case Some(l) =>
l.getMethod(level, classOf[String], classOf[String], classOf[Throwable])
.invoke(null, tag, message, throwable)
case None =>
System.out.println(level + tag + message + throwable)
}
def v(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.v(tag, message, tr))
def d(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.d(tag, message, tr))
def i(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.i(tag, message, tr))
def w(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.w(tag, message, tr))
def e(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.e(tag, message, tr))
}
trait Log {
def v(tag: String, message: String, tr: Throwable = null)
def d(tag: String, message: String, tr: Throwable = null)
def i(tag: String, message: String, tr: Throwable = null)
def w(tag: String, message: String, tr: Throwable = null)
def e(tag: String, message: String, tr: Throwable = null)
}

View file

@ -1,6 +1,6 @@
package com.nutomic.ensichat.core.interfaces
object Settings {
object SettingsInterface {
val KeyUserName = "user_name"
val KeyUserStatus = "user_status"
@ -26,9 +26,9 @@ object Settings {
/**
* Interface for persistent storage of key value pairs.
*
* Must support at least storage of strings and integers.
* Must support at least storage of String, Int, Long.
*/
trait Settings {
trait SettingsInterface {
def put[T](key: String, value: T): Unit
def get[T](key: String, default: T): T

View file

@ -0,0 +1,87 @@
package com.nutomic.ensichat.core.internet
import java.io.{IOException, InputStream, OutputStream}
import java.net.{InetAddress, Socket}
import com.nutomic.ensichat.core.Message.ReadMessageException
import com.nutomic.ensichat.core.body.ConnectionInfo
import com.nutomic.ensichat.core.header.MessageHeader
import com.nutomic.ensichat.core.interfaces.Log
import com.nutomic.ensichat.core.{Address, Crypto, Message}
/**
* Encapsulates an active connection to another node.
*/
class InternetConnectionThread(socket: Socket, crypto: Crypto, onDisconnected: (InternetConnectionThread) => Unit,
onReceive: (Message, InternetConnectionThread) => Unit) extends Thread {
private val Tag = "InternetConnectionThread"
private val inStream: InputStream =
try {
socket.getInputStream
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
close()
null
}
private val outStream: OutputStream =
try {
socket.getOutputStream
} catch {
case e: IOException =>
Log.e(Tag, "Failed to open stream", e)
close()
null
}
def internetAddress(): InetAddress = {
socket.getInetAddress
}
override def run(): Unit = {
Log.i(Tag, "Connection opened to " + socket.getInetAddress)
send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type,
Address.Null, Address.Null, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
socket.setKeepAlive(true)
while (socket.isConnected) {
try {
if (inStream.available() > 0) {
val msg = Message.read(inStream)
onReceive(msg, this)
}
} catch {
case e @ (_: ReadMessageException | _: IOException) =>
Log.w(Tag, "Failed to read incoming message", e)
close()
return
}
}
close()
Log.d(Tag, "exited: " + socket.isConnected)
}
def send(msg: Message): Unit = {
try {
outStream.write(msg.write)
} catch {
case e: IOException => Log.e(Tag, "Failed to write message", e)
}
}
def close(): Unit = {
try {
socket.close()
} catch {
case e: IOException => Log.w(Tag, "Failed to close socket", e)
}
Log.d(Tag, "Connection to " + socket.getInetAddress + " closed")
onDisconnected(this)
}
}

View file

@ -0,0 +1,129 @@
package com.nutomic.ensichat.core.internet
import java.io.IOException
import java.net.{InetAddress, Socket}
import com.nutomic.ensichat.core.body.ConnectionInfo
import com.nutomic.ensichat.core.interfaces.{Log, TransmissionInterface}
import com.nutomic.ensichat.core.util.FutureHelper
import com.nutomic.ensichat.core.{Address, ConnectionHandler, Crypto, Message}
import scala.concurrent.ExecutionContext.Implicits.global
object InternetInterface {
val Port = 26344
val BootstrapNodes = Set("192.168.1.104:26344", // T420
"46.101.229.4:26344") // digital ocean
}
/**
* Handles all Internet connectivity.
*
* TODO: send message through internet node: android -> server -> android
* TODO: send test reply to incoming messages
* TODO: send a list of all connected nodes when we connect to a node
*/
class InternetInterface(connectionHandler: ConnectionHandler, crypto: Crypto)
extends TransmissionInterface {
private val Tag = "InternetInterface"
private lazy val serverThread = new InternetServerThread(crypto, onConnected, onDisconnected, onReceiveMessage)
private var connections = Set[InternetConnectionThread]()
private var addressDeviceMap = Map[Address, InternetConnectionThread]()
/**
* Initializes and starts discovery and listening.
*/
override def create(): Unit = {
FutureHelper {
serverThread.start()
InternetInterface.BootstrapNodes.foreach(openConnection)
}
}
/**
* Stops discovery and listening.
*/
override def destroy(): Unit = {
serverThread.cancel()
connections.foreach(_.close())
}
private def openConnection(addressPort: String): Unit = {
val split = addressPort.split(":")
openConnection(split(0), split(1).toInt)
}
/**
* Opens connection to the specified IP address in client mode.
*/
private def openConnection(nodeAddress: String, port: Int): Unit = {
try {
val socket = new Socket(InetAddress.getByName(nodeAddress), port)
val ct = new InternetConnectionThread(socket, crypto, onDisconnected, onReceiveMessage)
connections += ct
ct.start()
} catch {
case e: IOException =>
Log.w(Tag, "Failed to open connection to " + nodeAddress + ":" + port, e)
}
}
private def onConnected(connectionThread: InternetConnectionThread): Unit = {
connections += connectionThread
}
private def onDisconnected(connectionThread: InternetConnectionThread): Unit = {
addressDeviceMap.find(_._2 == connectionThread).foreach { ad =>
Log.d(Tag, "Connection closed to " + ad._1)
connections -= connectionThread
addressDeviceMap -= ad._1
connectionHandler.onConnectionClosed()
}
}
private def onReceiveMessage(msg: Message, thread: InternetConnectionThread): Unit = msg.body match {
case info: ConnectionInfo =>
val address = crypto.calculateAddress(info.key)
if (address == crypto.localAddress) {
Log.i(Tag, "Address " + address + " is me, not connecting to myself")
thread.close()
return
}
// Service.onConnectionOpened sends message, so mapping already needs to be in place.
addressDeviceMap += (address -> thread)
if (!connectionHandler.onConnectionOpened(msg))
addressDeviceMap -= address
case _ =>
connectionHandler.onMessageReceived(msg)
}
/**
* Sends the message to nextHop.
*/
override def send(nextHop: Address, msg: Message): Unit = {
addressDeviceMap
.find(_._1 == nextHop)
.foreach(_._2.send(msg))
}
/**
* Returns all active Internet connections.
*/
override def getConnections = addressDeviceMap.keySet
def connectionChanged(): Unit = {
FutureHelper {
Log.i(Tag, "Network has changed. Close all connections and connect to bootstrap nodes again")
connections.foreach(_.close())
InternetInterface.BootstrapNodes.foreach(openConnection)
}
}
}

View file

@ -0,0 +1,44 @@
package com.nutomic.ensichat.core.internet
import java.io.{IOException, PrintStream}
import java.net.{Socket, ServerSocket}
import com.nutomic.ensichat.core.{Message, Crypto}
import com.nutomic.ensichat.core.interfaces.Log
import scala.io.BufferedSource
class InternetServerThread(crypto: Crypto, onConnected: (InternetConnectionThread) => Unit,
onDisconnected: (InternetConnectionThread) => Unit, onReceive: (Message, InternetConnectionThread) => Unit) extends Thread {
private val Tag = "InternetServerThread"
private lazy val socket: Option[ServerSocket] = try {
Option(new ServerSocket(InternetInterface.Port))
} catch {
case e: IOException =>
Log.w(Tag, "Failed to create server socket", e)
None
}
override def run(): Unit = {
try {
while (socket.get.isBound) {
val connection = new InternetConnectionThread(socket.get.accept(), crypto, onDisconnected, onReceive)
onConnected(connection)
connection.start()
}
} catch {
case e: IOException => Log.w(Tag, "Failed to accept connection", e)
}
}
def cancel(): Unit = {
try {
socket.get.close()
} catch {
case e: IOException => Log.w(Tag, "Failed to close socket", e)
}
}
}

View file

@ -5,7 +5,7 @@ import java.nio.ByteBuffer
/**
* Provides various helper methods for [[ByteBuffer]].
*/
object BufferUtils {
private[core] object BufferUtils {
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort

View file

@ -21,7 +21,7 @@ object FutureHelper {
// cross-platform way to execute on the foreground thread.
// We use this to make sure exceptions are not hidden in the logs.
Log.e(Tag, "Exception in Future", e)
System.exit(-1)
//System.exit(-1)
}
f
}

View file

@ -2,13 +2,13 @@ package com.nutomic.ensichat.core
import java.io.File
import com.nutomic.ensichat.core.interfaces.Settings
import com.nutomic.ensichat.core.interfaces.SettingsInterface
import junit.framework.TestCase
import org.junit.Assert._
object CryptoTest {
class TestSettings extends Settings {
class TestSettings extends SettingsInterface {
private var map = Map[String, Any]()
override def get[T](key: String, default: T): T = map.getOrElse(key, default).asInstanceOf[T]
override def put[T](key: String, value: T): Unit = map += (key -> value)

View file

@ -15,4 +15,7 @@
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# org.gradle.parallel=true
versionName=0.1.7
versionCode=8

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

23
server/build.gradle Normal file
View file

@ -0,0 +1,23 @@
apply plugin: 'scala'
apply plugin: 'application'
dependencies {
compile 'org.scala-lang:scala-library:2.11.7'
compile project(path: ':core')
compile 'com.github.scopt:scopt_2.10:3.3.0'
}
mainClassName = 'com.nutomic.ensichat.server.Main'
version = properties.versionName
applicationName = 'ensichat'
run {
// Use this to pass command line arguments via `gradle run`.
// Example:
// ```
// ./gradlew server:run -Pargs="--help"
// ```
if (project.hasProperty('args')) {
args project.args.split('\\s+')
}
}

View file

@ -0,0 +1,3 @@
package com.nutomic.ensichat.server
case class Config(name: Option[String] = None)

View file

@ -0,0 +1,23 @@
package com.nutomic.ensichat.server
import com.nutomic.ensichat.core.interfaces.DatabaseInterface
import com.nutomic.ensichat.core.{Address, Message, User}
class Database extends DatabaseInterface {
private var contacts = Set[User]()
def onMessageReceived(msg: Message): Unit = {}
def getContacts: Set[User] = contacts
def getContact(address: Address): Option[User] = contacts.find(_.address == address)
def addContact(contact: User): Unit = contacts += contact
def updateContact(contact: User): Unit = {
contacts = contacts.filterNot(_.address == contact.address)
contacts += contact
}
}

View file

@ -0,0 +1,42 @@
package com.nutomic.ensichat.server
import java.io.{PrintWriter, StringWriter}
import java.text.DateFormat
import java.util.{Date, Locale}
import com.nutomic.ensichat.core.interfaces.Log
import scala.collection.mutable
class Logging extends Log {
private val logs = new mutable.Queue[String]()
def dequeue(): Seq[String] = logs.dequeueAll((String) => true)
private def enqueue(tag: String, message: String, tr: Option[Throwable]): Unit = {
val df = DateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.UK)
val throwableString = tr.map { tr =>
val sw = new StringWriter()
tr.printStackTrace(new PrintWriter(sw))
"\n" + sw.toString
}
logs.enqueue(df.format(new Date()) + " " + tag + ": " + message + throwableString.getOrElse(""))
}
def v(tag: String, message: String, tr: Throwable = null): Unit =
enqueue("V/" + tag, message, Option(tr))
def d(tag: String, message: String, tr: Throwable = null): Unit =
enqueue("D/" + tag, message, Option(tr))
def i(tag: String, message: String, tr: Throwable = null): Unit =
enqueue("I/" + tag, message, Option(tr))
def w(tag: String, message: String, tr: Throwable = null): Unit =
enqueue("W/" + tag, message, Option(tr))
def e(tag: String, message: String, tr: Throwable = null): Unit =
enqueue("E/" + tag, message, Option(tr))
}

View file

@ -0,0 +1,74 @@
package com.nutomic.ensichat.server
import java.io.File
import java.util.concurrent.TimeUnit
import com.nutomic.ensichat.core.body.Text
import com.nutomic.ensichat.core.interfaces.{CallbackInterface, Log, SettingsInterface}
import com.nutomic.ensichat.core.{Message, ConnectionHandler, Crypto}
import scopt.OptionParser
object Main extends App with CallbackInterface {
private val Tag = "Main"
private val ConfigFolder = new File(System.getProperty("user.home"), ".config/ensichat")
private val ConfigFile = new File(ConfigFolder, "config.properties")
private val KeyFolder = new File(ConfigFolder, "keys")
private val LogInterval = TimeUnit.SECONDS.toMillis(1)
private lazy val logInstance = new Logging()
private lazy val settings = new Settings(ConfigFile)
private lazy val crypto = new Crypto(settings, KeyFolder)
private lazy val connectionHandler = new ConnectionHandler(settings, new Database(), this, crypto)
init()
private def init(): Unit = {
ConfigFolder.mkdirs()
KeyFolder.mkdirs()
Log.setLogInstance(logInstance)
sys.addShutdownHook(connectionHandler.stop())
val parser = new OptionParser[Config]("ensichat") {
head("ensichat")
opt[String]('n', "name") action { (x, c) =>
c.copy(name = Option(x))
} text "the username for this node (optional)"
help("help") text "prints this usage text"
}
parser.parse(args, Config()).foreach { config =>
config.name.foreach(settings.put(SettingsInterface.KeyUserName, _))
run()
}
}
private def run(): Unit = {
connectionHandler.start()
// Keep alive and print logs
while (true) {
Thread.sleep(LogInterval)
logInstance.dequeue().foreach(System.out.println)
}
}
def onMessageReceived(msg: Message): Unit = {
if (msg.header.target != crypto.localAddress)
return
msg.body match {
case text: Text =>
val address = msg.header.origin
val name = connectionHandler.getUser(address).name
connectionHandler.sendTo(address, new Text("Hello " + name))
Log.i(Tag, "Received text: " + text.text)
case _ =>
Log.i(Tag, "Received msg: " + msg.body)
}
}
def onConnectionsChanged(): Unit = {}
}

View file

@ -0,0 +1,53 @@
package com.nutomic.ensichat.server
import java.io._
import java.util.Properties
import com.nutomic.ensichat.core.interfaces.{Log, SettingsInterface}
import scala.collection.JavaConverters._
class Settings(file: File) extends SettingsInterface {
private val Tag = "Settings"
if (!file.exists()) {
file.createNewFile()
put(SettingsInterface.KeyUserName, "unknown user")
}
private lazy val props: Properties = {
val p = new Properties()
try {
val fis = new InputStreamReader(new FileInputStream(file), "UTF-8")
p.load(fis)
fis.close()
} catch {
case e: IOException => Log.w(Tag, "Failed to load settings from " + file, e)
}
p
}
def put[T](key: String, value: T): Unit = {
props.asScala.put(key, value.toString)
try {
val fos = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")
props.store(fos, "")
fos.close()
} catch {
case e: IOException => Log.w(Tag, "Failed to write preference for key " + key, e)
}
}
def get[T](key: String, default: T): T = {
val value = props.asScala.getOrElse[String](key, default.toString)
val cast = default match {
case _: Int => value.toInt
case _: Long => value.toLong
case _: String => value
}
// This has no effect due to type erasure, but is needed to avoid compiler error.
cast.asInstanceOf[T]
}
}

View file

@ -1 +1 @@
include ':android', ':core'
include ':android', ':core', ':server'