Moved remaining messages to binary encoding, removed msgpack dependency.

This seems to have added some random disconnects, but there doesn't
seem to be a reason for this in the code.
This commit is contained in:
Felix Ableitner 2014-12-12 00:13:22 +02:00
parent de86f5f121
commit 834f3ca724
43 changed files with 984 additions and 585 deletions

View file

@ -7,17 +7,17 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
document are to be interpreted as described in RFC 2119.
A node is a single device implementing this protocol. Each node has
exactl one node address.
A _node_ is a single device implementing this protocol. Each node has
exactly one node address based on its RSA key pair.
A node address consists of 32 bytes and is the SHA-256 hash of the
A _node address_ consists of 32 bytes and is the SHA-256 hash of the
node's public key.
The broadcast address is
The _broadcast address_ is
`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF`
(i.e. all bits set).
The null address is
The _null address_ is
`0x0000000000000000000000000000000000000000000000000000000000000000`
(i.e. no bits set).
@ -29,6 +29,27 @@ nodes MUST NOT connect to a node with either address.
Messages
--------
All messages are signed using RSASSA-PKCS1-v1_5. All messages except
ConnectionInfo are encrypted using AES/CBC/PKCS5Padding, after which
the AES key is wrapped with the recipient's public RSA key.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Header (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Encryption Data (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Body (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
### Header
Every message starts with one 32 bit word indicating the message
@ -54,6 +75,8 @@ header is in network byte order, i.e. big endian.
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number | Metric | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Body Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Ver specifies the protocol version number. This is currently 0. A
message with unknown version number MUST be ignored. The connection
@ -83,17 +106,50 @@ Sequence number is the sequence number of either the source or target
node for this message, depending on type.
### Encryption Data
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Signature Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Signature (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Encryption Key Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Encryption Key (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Encryption key is the symmetric key that was used to encrypt the message
body.
Signature is the cryptographic signature over the (unencrypted) message
header and message body.
ConnectionInfo (Type = 0)
---------
After successfully connecting to a node via Bluetooth, public keys
must be exchanged. Each node MUST send this as the first message over
are exchanged. Each node MUST send this as the first message over
the connection. Hop Limit MUST be 1 for this message type (i.e. it
must never be forwarded). Origin Address and Target Address MUST be
set to all zeros, and MUST be ignored by the receiving node.
A receiving node SHOULD store the key in permanent storage if it
hasn't already stored it earlier. This key is to be used for message
hasn't already stored it earlier. However, a node MAY decide to
delete these stored keys in a least-recently-used order to adhere
to storage limitations. If a key has been deleted, messages to
that node can only be sent once a new ConnectionInfo message
for it has been received.
This key is to be used for message
encryption when communicating with the sending node.
0 1 2 3
@ -114,20 +170,46 @@ After this message has been received, communication with normal messages
may start.
### Data (Data Transfer, Type = 255)
### RequestAddContact (Type = 4)
Sent when a user wants to add another node as a contact. After this,
a ResultAddContact message should be returned.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length |
| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
### ResultAddContact (Type = 5)
Sent as response to a RequestAddContact message.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|A| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Accepted bit (A) is true if the user accepts the new contact, false
otherwise. Nodes should only add another node as a contact if both
users agreed.
### Text (Type = 6)
A simple chat message.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Text Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Data (variable length) \
\ Text (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Length is the number of bytes in data.
Data is any binary data that should be transported.
This message type is deprecated.
Text the string to be transferred, encoded as UTF-8.

View file

@ -6,27 +6,19 @@ buildscript {
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:0.14.4"
classpath "jp.leafytree.gradle:gradle-android-scala-plugin:1.3.1"
}
}
dependencies {
compile "com.android.support:support-v4:21.0.0"
compile "com.android.support:support-v4:21.0.2"
// For `flat` debug config, as `flatProvided` is unknown.
provided "org.scala-lang:scala-library:2.11.4"
debugCompile "org.scala-lang:scala-library:2.11.4"
releaseCompile "org.scala-lang:scala-library:2.11.4"
compile("org.msgpack:msgpack-scala_2.11:0.6.11") {
transitive = false;
}
compile('org.msgpack:msgpack:0.6.11') {
transitive = false;
}
compile 'com.google.guava:guava:18.0'
}
android {
compileSdkVersion 21
buildToolsVersion "21.1.1"
@ -40,13 +32,8 @@ android {
}
sourceSets {
main {
scala.srcDir "src/main/scala"
}
androidTest {
scala.srcDir "src/androidTest/scala"
}
main.scala.srcDir "src/main/scala"
androidTest.scala.srcDir "src/androidTest/scala"
}
buildTypes {
@ -68,11 +55,13 @@ android {
// Needed to rename `app-thin.apk` to `app-debug.apk` (because Android Studio doesn't let us
// specify a different apk name).
/*
applicationVariants.all { variant ->
def apk = variant.outputFile;
def newName = apk.name.replace("app-thin", "app-debug");
variant.outputFile = new File(apk.parentFile, newName);
}
*/
// Avoid duplicate file errors during packaging.
packagingOptions {
@ -82,3 +71,7 @@ android {
exclude 'META-INF/NOTICE'
}
}
tasks.withType(ScalaCompile) {
scalaCompileOptions.useCompileDaemon = true
}

View file

@ -18,11 +18,12 @@
-dontpreverify
-dontwarn scala.**
-keep class !scala*.** { *; }
-ignorewarnings
# Avoid crash when invoking String.toInt (see https://issues.scala-lang.org/browse/SI-5397).
-keep class scala.collection.SeqLike {
public protected *;
}
# Suppress warnings caused by msgpack (code works fine anyway).
-dontwarn
# Disable warnings for Guava annotations.
-dontwarn javax.annotation.**
-dontwarn javax.inject.**
-dontwarn sun.misc.Unsafe

View file

@ -14,14 +14,17 @@ object AddressTest {
val a4 = new Address("4444459893F8810C4024CFC951374AABA1F4DE6347A3D7D8E44918AD1FF2BA36")
val a1Binary: Array[Byte] = Array(-91, 27, 116, 71, 94, -26, 34, -61, -55, 36, -37, 20, 118, 104, -8, 94, 2, 76, -96, -76, 76, -95, 70, -75, -29, -45, -61, 26, 84, -77, 76, 30).map(_.toByte)
val a1Binary: Array[Byte] = Array(-91, 27, 116, 71, 94, -26, 34, -61, -55, 36, -37, 20, 118, 104,
-8, 94, 2, 76, -96, -76, 76, -95, 70, -75, -29, -45, -61, 26, 84, -77, 76, 30).map(_.toByte)
val Addresses = Set(a1, a2, a3, a4, Address.Broadcast, Address.Null)
}
class AddressTest extends AndroidTestCase {
def testEncode(): Unit = {
Set(Address.Broadcast, Address.Null, a1, a2, a3, a4).foreach{a =>
Addresses.foreach{a =>
val base32 = a.toString
val read = new Address(base32)
assertEquals(a, read)

View file

@ -1,30 +1,41 @@
package com.nutomic.ensichat.aodvv2
import java.util.Date
import java.util.GregorianCalendar
import android.test.AndroidTestCase
import junit.framework.Assert
import com.nutomic.ensichat.aodvv2.MessageHeaderTest._
import junit.framework.Assert._
object MessageHeaderTest {
val h1 = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit, new Date(), AddressTest.a3,
AddressTest.a4, 456, 123)
val h1 = new MessageHeader(Text.Type, MessageHeader.DefaultHopLimit, AddressTest.a1,
AddressTest.a2, 1234, 0, new GregorianCalendar(1970, 1, 1).getTime, 567, 8)
val h2 = new MessageHeader(0xfff, 0, new Date(0xffffffff), Address.Null, Address.Broadcast, 0,
0xff)
val h2 = new MessageHeader(Text.Type, 0, AddressTest.a1, AddressTest.a3, 8765, 234,
new GregorianCalendar(2014, 6, 10).getTime, 0, 0xff)
val h3 = new MessageHeader(0xfff, 0xff, new Date(0), Address.Broadcast, Address.Null, 0xffff, 0)
val h3 = new MessageHeader(Text.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56,
new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0)
val h4 = new MessageHeader(0xfff, 0, Address.Null, Address.Broadcast, 0, 0xff,
new GregorianCalendar(1990, 1, 1).getTime, 0, 0xff)
val h5 = new MessageHeader(ConnectionInfo.Type, 0xff, Address.Broadcast, Address.Null, 0xffff, 0,
new GregorianCalendar(2035, 12, 31).getTime, 0xffff, 0)
val headers = Set(h1, h2, h3, h4, h5)
}
class MessageHeaderTest extends AndroidTestCase {
def testSerialize(): Unit = {
val ci = ConnectionInfoTest.generateCi(getContext)
val bytes = MessageHeaderTest.h1.write(ci)
val header = MessageHeader.read(bytes)
Assert.assertEquals(MessageHeaderTest.h1, header)
Assert.assertEquals(bytes.length, header.Length)
headers.foreach{h =>
val bytes = h.write(0)
val header = MessageHeader.read(bytes)
assertEquals(h, header)
assertEquals(bytes.length, header.Length)
}
}
}

View file

@ -0,0 +1,77 @@
package com.nutomic.ensichat.aodvv2
import java.io.ByteArrayInputStream
import java.util.GregorianCalendar
import android.test.AndroidTestCase
import com.nutomic.ensichat.aodvv2.MessageHeaderTest._
import com.nutomic.ensichat.aodvv2.MessageTest._
import com.nutomic.ensichat.messages.Crypto
import junit.framework.Assert._
import scala.collection.immutable.TreeSet
object MessageTest {
val m1 = new Message(h1, new Text("first"))
val m2 = new Message(h2, new Text("second"))
val m3 = new Message(h3, new Text("third"))
val messages = Set(m1, m2, m3)
}
class MessageTest extends AndroidTestCase {
lazy val Crypto: Crypto = new Crypto(getContext)
override def setUp(): Unit = {
super.setUp()
if (!Crypto.localKeysExist) {
Crypto.generateLocalKeys()
}
}
def testOrder(): Unit = {
var messages = new TreeSet[Message]()(Message.Ordering)
messages += m1
messages += m2
assertEquals(m1, messages.firstKey)
messages = new TreeSet[Message]()(Message.Ordering)
messages += m2
messages += m3
assertEquals(m2, messages.firstKey)
}
def testSerializeSigned(): Unit = {
val header = new MessageHeader(ConnectionInfo.Type, 0xff, AddressTest.a4, AddressTest.a2, 0, 56,
new GregorianCalendar(2020, 11, 11).getTime, 0xffff, 0)
val m = new Message(header, ConnectionInfoTest.generateCi(getContext))
val signed = Crypto.sign(m)
val bytes = signed.write
val read = Message.read(new ByteArrayInputStream(bytes))
assertEquals(signed, read)
assertTrue(Crypto.verify(read, Crypto.getLocalPublicKey))
}
def testSerializeEncrypted(): Unit = {
messages.foreach{ m =>
val signed = Crypto.sign(m)
val encrypted = Crypto.encrypt(signed, Crypto.getLocalPublicKey)
val bytes = encrypted.write
val read = Message.read(new ByteArrayInputStream(bytes))
assertEquals(encrypted.Crypto, read.Crypto)
val decrypted = Crypto.decrypt(read)
assertEquals(m.Header, decrypted.Header)
assertEquals(m.Body, decrypted.Body)
assertTrue(Crypto.verify(decrypted, Crypto.getLocalPublicKey))
}
}
}

View file

@ -0,0 +1,18 @@
package com.nutomic.ensichat.aodvv2
import android.test.AndroidTestCase
import junit.framework.Assert._
class ResultAddContactTest extends AndroidTestCase {
def testWriteRead(): Unit = {
Array(true, false).foreach { a =>
val rac = new ResultAddContact(a)
val bytes = rac.write
val read = ResultAddContact.read(bytes)
assertEquals(a, read.Accepted)
}
}
}

View file

@ -1,7 +1,7 @@
package com.nutomic.ensichat.messages
import android.test.AndroidTestCase
import com.nutomic.ensichat.messages.MessageTest._
import com.nutomic.ensichat.aodvv2.MessageTest._
import junit.framework.Assert._
class CryptoTest extends AndroidTestCase {
@ -16,15 +16,21 @@ class CryptoTest extends AndroidTestCase {
}
def testSignVerify(): Unit = {
val sig = Crypto.calculateSignature(m1)
assertTrue(Crypto.isValidSignature(m1, sig, Crypto.getLocalPublicKey))
messages.foreach { m =>
val signed = Crypto.sign(m)
assertTrue(Crypto.verify(signed, Crypto.getLocalPublicKey))
assertEquals(m.Header, signed.Header)
assertEquals(m.Body, signed.Body)
}
}
def testEncryptDecrypt(): Unit = {
val (encrypted, key) =
Crypto.encrypt(null, MessageTest.m1.write(Array[Byte]()), Crypto.getLocalPublicKey)
val decrypted = Crypto.decrypt(encrypted, key)
assertEquals(MessageTest.m1, Message.read(decrypted)._1)
messages.foreach{ m =>
val encrypted = Crypto.encrypt(Crypto.sign(m), Crypto.getLocalPublicKey)
val decrypted = Crypto.decrypt(encrypted)
assertEquals(m.Body, decrypted.Body)
assertEquals(m.Header, encrypted.Header)
}
}
}

View file

@ -1,50 +0,0 @@
package com.nutomic.ensichat.messages
import java.io.{PipedInputStream, PipedOutputStream}
import java.util.GregorianCalendar
import android.test.AndroidTestCase
import com.nutomic.ensichat.aodvv2.AddressTest
import com.nutomic.ensichat.messages.MessageTest._
import junit.framework.Assert._
import scala.collection.immutable.TreeSet
object MessageTest {
val m1 = new TextMessage(AddressTest.a1, AddressTest.a2,
new GregorianCalendar(2014, 10, 29).getTime, "first")
val m2 = new TextMessage(AddressTest.a1, AddressTest.a3,
new GregorianCalendar(2014, 10, 30).getTime, "second")
val m3 = new TextMessage(AddressTest.a4, AddressTest.a2,
new GregorianCalendar(2014, 10, 31).getTime, "third")
}
class MessageTest extends AndroidTestCase {
def testSerialize(): Unit = {
Set(m1, m2, m3).foreach { m =>
val pis = new PipedInputStream()
val pos = new PipedOutputStream(pis)
val bytes = m.write(Array[Byte]())
val (msg, _) = Message.read(bytes)
assertEquals(m, msg)
}
}
def testOrder(): Unit = {
var messages = new TreeSet[Message]()(Message.Ordering)
messages += MessageTest.m1
messages += MessageTest.m2
assertEquals(MessageTest.m1, messages.firstKey)
messages = new TreeSet[Message]()(Message.Ordering)
messages += MessageTest.m2
messages += MessageTest.m3
assertEquals(MessageTest.m2, messages.firstKey)
}
}

View file

@ -9,7 +9,7 @@ import android.database.sqlite.SQLiteDatabase
import android.test.AndroidTestCase
import android.test.mock.MockContext
import com.nutomic.ensichat.aodvv2.AddressTest
import com.nutomic.ensichat.messages.MessageTest
import com.nutomic.ensichat.aodvv2.MessageTest._
import junit.framework.Assert._
class DatabaseTest extends AndroidTestCase {
@ -27,9 +27,9 @@ class DatabaseTest extends AndroidTestCase {
private lazy val Database = new Database(new TestContext(getContext))
override def setUp(): Unit = {
Database.addMessage(MessageTest.m1)
Database.addMessage(MessageTest.m2)
Database.addMessage(MessageTest.m3)
Database.addMessage(m1)
Database.addMessage(m2)
Database.addMessage(m3)
}
override def tearDown(): Unit = {
@ -38,22 +38,22 @@ class DatabaseTest extends AndroidTestCase {
}
def testMessageCount(): Unit = {
val msg1 = Database.getMessages(MessageTest.m1.sender, 1)
val msg1 = Database.getMessages(m1.Header.Origin, 1)
assertEquals(1, msg1.size)
val msg2 = Database.getMessages(MessageTest.m1.sender, 3)
val msg2 = Database.getMessages(m1.Header.Origin, 3)
assertEquals(2, msg2.size)
}
def testMessageOrder(): Unit = {
val msg = Database.getMessages(MessageTest.m1.receiver, 1)
assertTrue(msg.contains(MessageTest.m3))
val msg = Database.getMessages(m1.Header.Target, 1)
assertTrue(msg.contains(m3))
}
def testMessageSelect(): Unit = {
val msg = Database.getMessages(MessageTest.m1.receiver, 2)
assertTrue(msg.contains(MessageTest.m1))
assertTrue(msg.contains(MessageTest.m3))
val msg = Database.getMessages(m1.Header.Target, 2)
assertTrue(msg.contains(m1))
assertTrue(msg.contains(m3))
}
def testAddContact(): Unit = {

View file

@ -5,7 +5,7 @@
<EditTextPreference
android:title="@string/scan_interval_seconds"
android:key="scan_interval_seconds"
android:defaultValue="5"
android:defaultValue="15"
android:inputType="number"
android:numeric="integer" />

View file

@ -1,7 +1,5 @@
package com.nutomic.ensichat.activities
import java.util.Date
import android.app.AlertDialog
import android.content.DialogInterface.OnClickListener
import android.content.{Context, DialogInterface}
@ -12,10 +10,10 @@ import android.view._
import android.widget.AdapterView.OnItemClickListener
import android.widget._
import com.nutomic.ensichat.R
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.aodvv2.{Address, Message, RequestAddContact, ResultAddContact}
import com.nutomic.ensichat.bluetooth.ChatService
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.messages.{Crypto, Message, RequestAddContactMessage, ResultAddContactMessage}
import com.nutomic.ensichat.messages.Crypto
import com.nutomic.ensichat.util.{DevicesAdapter, IdenticonGenerator}
import scala.collection.SortedSet
@ -92,7 +90,7 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
return
}
service.send(new RequestAddContactMessage(Crypto.getLocalAddress, address, new Date()))
service.sendTo(address, new RequestAddContact())
addDeviceDialog(address)
}
@ -108,12 +106,10 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
currentlyAdding +=
(address -> new AddContactInfo(currentlyAdding(address).localConfirmed, true))
addContactIfBothConfirmed(address)
service.send(
new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), true))
service.sendTo(address, new ResultAddContact(true))
case DialogInterface.BUTTON_NEGATIVE =>
// Local user denied adding contact, send info to other device.
service.send(
new ResultAddContactMessage(Crypto.getLocalAddress, address, new Date(), false))
service.sendTo(address, new ResultAddContact(false))
}
}
@ -137,27 +133,28 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnNearbyCont
}
/**
* Handles incoming [[RequestAddContactMessage]] and [[ResultAddContactMessage]] messages.
* Handles incoming [[RequestAddContact]] and [[ResultAddContact]] messages.
*
* These are only handled here and require user action, so contacts can only be added if
* the user is in this activity.
*/
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
messages.filter(_.receiver == Crypto.getLocalAddress)
messages.filter(_.Header.Target == Crypto.getLocalAddress)
.foreach{
case m: RequestAddContactMessage =>
Log.i(Tag, "Remote device " + m.sender + " wants to add us as a contact, showing dialog")
addDeviceDialog(m.sender)
case m: ResultAddContactMessage =>
if (m.Accepted) {
Log.i(Tag, "Remote device " + m.sender + " accepted us as a contact, updating state")
currentlyAdding += (m.sender ->
new AddContactInfo(true, currentlyAdding(m.sender).remoteConfirmed))
addContactIfBothConfirmed(m.sender)
case m if m.Body.isInstanceOf[RequestAddContact] =>
Log.i(Tag, "Remote device " + m.Header.Origin + " wants to add us as a contact, showing dialog")
addDeviceDialog(m.Header.Origin)
case m if m.Body.isInstanceOf[ResultAddContact] =>
val origin = m.Header.Origin
if (m.Body.asInstanceOf[ResultAddContact].Accepted) {
Log.i(Tag, "Remote device " + origin + " accepted us as a contact, updating state")
currentlyAdding += (origin ->
new AddContactInfo(true, currentlyAdding(origin).remoteConfirmed))
addContactIfBothConfirmed(origin)
} else {
Log.i(Tag, "Remote device " + m.sender + " denied us as a contact, showing toast")
Log.i(Tag, "Remote device " + origin + " denied us as a contact, showing toast")
Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
currentlyAdding -= m.sender
currentlyAdding -= origin
}
case _ =>
}

View file

@ -34,11 +34,22 @@ object ConnectionInfo {
*/
class ConnectionInfo(val key: PublicKey) extends MessageBody {
override def Type = ConnectionInfo.Type
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(4 + key.getEncoded.length)
val b = ByteBuffer.allocate(length)
BufferUtils.putUnsignedInt(b, key.getEncoded.length)
b.put(key.getEncoded)
b.array()
}
override def equals(a: Any): Boolean = a match {
case o: ConnectionInfo => key == o.key
case _ => false
}
override def toString = "ConnectionInfo(key=" + key + ")"
override def length = 4 + key.getEncoded.length
}

View file

@ -0,0 +1,67 @@
package com.nutomic.ensichat.aodvv2
import java.nio.ByteBuffer
import java.util.Arrays
import com.nutomic.ensichat.util.BufferUtils
object CryptoData {
/**
* Constructs [[CryptoData]] instance from byte array.
*/
def read(array: Array[Byte]): (CryptoData, Array[Byte]) = {
val b = ByteBuffer.wrap(array)
val signatureLength = BufferUtils.getUnsignedInt(b).toInt
val signature = new Array[Byte](signatureLength)
b.get(signature, 0, signatureLength)
val keyLength = BufferUtils.getUnsignedInt(b).toInt
val key =
if (keyLength != 0) {
val key = new Array[Byte](keyLength)
b.get(key, 0, keyLength)
Some(key)
}
else None
val remaining = new Array[Byte](b.remaining())
b.get(remaining, 0, b.remaining())
(new CryptoData(Some(signature), key), remaining)
}
}
/**
* Holds the signature and (optional) key that are stored in a message.
*/
class CryptoData(val Signature: Option[Array[Byte]], val Key: Option[Array[Byte]]) {
override def equals(a: Any): Boolean = a match {
case o: CryptoData =>
Arrays.equals(Signature.orNull, o.Signature.orNull) && Arrays.equals(Key.orNull, o.Key.orNull)
case _ => false
}
/**
* Writes this object into a new byte array.
* @return
*/
def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
BufferUtils.putUnsignedInt(b, Signature.get.length)
b.put(Signature.get)
BufferUtils.putUnsignedInt(b, keyLength)
if (Key.nonEmpty) b.put(Key.get)
b.array()
}
def length = 8 + Signature.get.length + keyLength
private def keyLength = if (Key.isDefined) Key.get.length else 0
override def toString = "CryptoData(Signature.length=" + Signature.foreach(_.length) +
", Key.length=" + Key.foreach(_.length) + ")"
}

View file

@ -1,37 +0,0 @@
package com.nutomic.ensichat.aodvv2
import java.nio.ByteBuffer
import com.nutomic.ensichat.util.BufferUtils
object Data {
val Type = 255
/**
* Constructs [[Data]] object from byte array.
*/
def read(array: Array[Byte]): Data = {
val b = ByteBuffer.wrap(array)
val length = BufferUtils.getUnsignedInt(b).toInt
val data = new Array[Byte](length)
b.get(data, 0, length)
new Data(data)
}
}
/**
* Container for [[com.nutomic.ensichat.messages.Message]] objects.
*/
@Deprecated
class Data(val data: Array[Byte]) extends MessageBody {
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(4 + data.length)
BufferUtils.putUnsignedInt(b, data.length)
b.put(data)
b.array()
}
}

View file

@ -0,0 +1,15 @@
package com.nutomic.ensichat.aodvv2
/**
* Represents the data in an encrypted message body.
*/
class EncryptedBody(val Data: Array[Byte]) extends MessageBody {
override def Type = -1
def write = Data
override def toString = "EncryptedBody(Data.length=" + Data.length + ")"
override def length = Data.length
}

View file

@ -0,0 +1,50 @@
package com.nutomic.ensichat.aodvv2
import java.io.InputStream
object Message {
/**
* Orders messages by date, oldest messages first.
*/
val Ordering = new Ordering[Message] {
override def compare(m1: Message, m2: Message) = m1.Header.Time.compareTo(m2.Header.Time)
}
def read(stream: InputStream): Message = {
val headerBytes = new Array[Byte](MessageHeader.Length)
stream.read(headerBytes, 0, MessageHeader.Length)
val header = MessageHeader.read(headerBytes)
val contentLength = (header.Length - MessageHeader.Length).toInt
val contentBytes = new Array[Byte](contentLength)
stream.read(contentBytes, 0, contentLength)
val (crypto, remaining) = CryptoData.read(contentBytes)
val body =
header.MessageType match {
case ConnectionInfo.Type => ConnectionInfo.read(remaining)
case _ => new EncryptedBody(remaining)
}
new Message(header, crypto, body)
}
}
class Message(val Header: MessageHeader, val Crypto: CryptoData, val Body: MessageBody) {
def this(header: MessageHeader, body: MessageBody) =
this(header, new CryptoData(None, None), body)
def write = Header.write(Body.length + Crypto.length) ++ Crypto.write ++ Body.write
override def toString = "Message(Header=" + Header + ", Body=" + Body + ", Crypto=" + Crypto + ")"
override def equals(a: Any): Boolean = a match {
case o: Message => Header == o.Header && Body == o.Body && Crypto == o.Crypto
case _ => false
}
}

View file

@ -1,14 +1,19 @@
package com.nutomic.ensichat.aodvv2
import android.util.Log
/**
* Holds the actual message content.
*/
abstract class MessageBody {
def Type: Int
/**
* Writes the message contents to a byte array.
* @return
*/
def write: Array[Byte]
def length: Int
}

View file

@ -7,11 +7,11 @@ import com.nutomic.ensichat.util.BufferUtils
object MessageHeader {
val Length = 16 + 2 * Address.Length
val Length = 20 + 2 * Address.Length
val DefaultHopLimit = 20
val Version = 3
val Version = 0
class ParseMessageException(detailMessage: String) extends RuntimeException(detailMessage) {
}
@ -38,7 +38,7 @@ object MessageHeader {
val seqNum = BufferUtils.getUnsignedShort(b)
val metric = BufferUtils.getUnsignedByte(b)
new MessageHeader(messageType, hopLimit, time, origin, target, seqNum, metric, length, hopCount)
new MessageHeader(messageType, hopLimit, origin, target, seqNum, metric, time, length, hopCount)
}
}
@ -48,27 +48,26 @@ object MessageHeader {
*/
class MessageHeader(val MessageType: Int,
val HopLimit: Int,
val Time: Date,
val Origin: Address,
val Target: Address,
val SequenceNumber: Int,
val Metric: Int,
val Time: Date = new Date(),
val Length: Long = -1,
val HopCount: Int = 0) {
/**
* Writes the header to byte array.
*/
def write(body: MessageBody): Array[Byte] = {
def write(contentLength: Int): Array[Byte] = {
val b = ByteBuffer.allocate(MessageHeader.Length)
val bodyBytes = body.write
val versionAndType = (MessageHeader.Version << 12) | MessageType
BufferUtils.putUnsignedShort(b, versionAndType)
BufferUtils.putUnsignedByte(b, HopLimit)
BufferUtils.putUnsignedByte(b, HopCount)
BufferUtils.putUnsignedInt(b, MessageHeader.Length + bodyBytes.length)
BufferUtils.putUnsignedInt(b, MessageHeader.Length + contentLength)
b.putInt((Time.getTime / 1000).toInt)
b.put(Origin.Bytes)
b.put(Target.Bytes)
@ -77,7 +76,7 @@ class MessageHeader(val MessageType: Int,
BufferUtils.putUnsignedByte(b, Metric)
BufferUtils.putUnsignedByte(b, 0)
b.array() ++ bodyBytes
b.array()
}
override def equals(a: Any): Boolean = a match {
@ -89,9 +88,8 @@ class MessageHeader(val MessageType: Int,
Target == o.Target &&
SequenceNumber == o.SequenceNumber &&
Metric == o.Metric &&
// Don't compare length as it may be unknown (when header was just created without a body).
//Length == o.Length &&
HopCount == o.HopCount
// Don't compare length as it may be unknown (when header was just created without a body).
case _ => false
}

View file

@ -0,0 +1,34 @@
package com.nutomic.ensichat.aodvv2
import java.nio.ByteBuffer
object RequestAddContact {
val Type = 4
/**
* Constructs [[RequestAddContact]] instance from byte array.
*/
def read(array: Array[Byte]): RequestAddContact = {
new RequestAddContact()
}
}
/**
* Sent when the user initiates adding another device as a contact.
*/
class RequestAddContact extends MessageBody {
override def Type = RequestAddContact.Type
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
b.array()
}
override def toString = "RequestAddContact()"
override def length = 4
}

View file

@ -0,0 +1,41 @@
package com.nutomic.ensichat.aodvv2
import java.nio.ByteBuffer
import com.nutomic.ensichat.util.BufferUtils
object ResultAddContact {
val Type = 5
/**
* Constructs [[ResultAddContact]] instance from byte array.
*/
def read(array: Array[Byte]): ResultAddContact = {
val b = ByteBuffer.wrap(array)
val first = BufferUtils.getUnsignedByte(b)
val accepted = (first & 0x80) != 0
new ResultAddContact(accepted)
}
}
/**
* Contains the result of a [[RequestAddContact]] message.
*/
class ResultAddContact(val Accepted: Boolean) extends MessageBody {
override def Type = ResultAddContact.Type
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
BufferUtils.putUnsignedByte(b, if (Accepted) 0x80 else 0)
(0 to 1).foreach(_ => BufferUtils.putUnsignedByte(b, 0))
b.array()
}
override def toString = "ResultAddContact(Accepted=" + Accepted + ")"
override def length = 4
}

View file

@ -0,0 +1,50 @@
package com.nutomic.ensichat.aodvv2
import java.nio.ByteBuffer
import com.nutomic.ensichat.util.BufferUtils
object Text {
val Type = 6
val Charset = "UTF-8"
/**
* Constructs [[Text]] instance from byte array.
*/
def read(array: Array[Byte]): Text = {
val b = ByteBuffer.wrap(array)
val length = BufferUtils.getUnsignedInt(b).toInt
val bytes = new Array[Byte](length)
b.get(bytes, 0, length)
new Text(new String(bytes, Text.Charset))
}
}
/**
* Holds a plain text message.
*/
class Text(val text: String) extends MessageBody {
override def Type = Text.Type
override def write: Array[Byte] = {
val bytes = text.getBytes(Text.Charset)
val b = ByteBuffer.allocate(4 + bytes.length)
BufferUtils.putUnsignedInt(b, bytes.length)
b.put(bytes)
b.array()
}
override def equals(a: Any): Boolean = a match {
case o: Text => text == o.text
case _ => false
}
override def toString = "Text(" + text + ")"
override def length = write.length
}

View file

@ -1,6 +1,6 @@
package com.nutomic.ensichat.bluetooth
import java.util.{Date, UUID}
import java.util.UUID
import android.app.Service
import android.bluetooth.{BluetoothAdapter, BluetoothDevice, BluetoothSocket}
@ -14,7 +14,6 @@ import com.nutomic.ensichat.aodvv2._
import com.nutomic.ensichat.bluetooth.ChatService.{OnMessageReceivedListener, OnNearbyContactsChangedListener}
import com.nutomic.ensichat.messages._
import com.nutomic.ensichat.util.Database
import org.msgpack.ScalaMessagePack
import scala.collection.SortedSet
import scala.collection.immutable.{HashMap, HashSet, TreeSet}
@ -77,6 +76,8 @@ class ChatService extends Service {
private lazy val Crypto = new Crypto(this)
private var discovered = Set[Device]()
private val AddressDeviceMap = HashBiMap.create[Address, Device.ID]()
/**
@ -89,6 +90,8 @@ class ChatService extends Service {
registerReceiver(DeviceDiscoveredReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND))
registerReceiver(BluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
registerReceiver(DiscoveryFinishedReceiver,
new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED))
if (bluetoothAdapter.isEnabled) {
startBluetoothConnections()
}
@ -116,6 +119,7 @@ class ChatService extends Service {
cancelDiscovery = true
unregisterReceiver(DeviceDiscoveredReceiver)
unregisterReceiver(BluetoothStateReceiver)
unregisterReceiver(DiscoveryFinishedReceiver)
}
/**
@ -126,12 +130,12 @@ class ChatService extends Service {
return
if (!bluetoothAdapter.isDiscovering) {
Log.v(Tag, "Running discovery")
Log.v(Tag, "Starting discovery")
bluetoothAdapter.startDiscovery()
}
val scanInterval = PreferenceManager.getDefaultSharedPreferences(this)
.getString("scan_interval_seconds", "5").toInt * 1000
.getString("scan_interval_seconds", "15").toInt * 1000
MainHandler.postDelayed(new Runnable {
override def run(): Unit = discover()
}, scanInterval)
@ -142,10 +146,21 @@ class ChatService extends Service {
*/
private val DeviceDiscoveredReceiver = new BroadcastReceiver() {
override def onReceive(context: Context, intent: Intent) {
val device: Device =
new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
devices += (device.Id -> device)
new ConnectThread(device, onConnectionChanged).start()
discovered += new Device(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE), false)
}
}
/**
* Iniates the actual connection to discovered devices.
*/
private val DiscoveryFinishedReceiver = new BroadcastReceiver() {
override def onReceive(context: Context, intent: Intent): Unit = {
discovered.filterNot(d => connections.keySet.contains(d.Id))
.foreach { d =>
new ConnectThread(d, onConnectionChanged).start()
devices += (d.Id -> d)
}
discovered = Set[Device]()
}
}
@ -200,7 +215,7 @@ class ChatService extends Service {
def onConnectionChanged(device: Device, socket: BluetoothSocket): Unit = {
devices += (device.Id -> device)
if (device.Connected) {
if (device.Connected && !connections.keySet.contains(device.Id)) {
connections += (device.Id ->
new TransferThread(device, socket, this, Crypto, onReceiveMessage))
connections(device.Id).start()
@ -219,79 +234,75 @@ class ChatService extends Service {
}
/**
* Sends message to the device specified as receiver,
* Sends a new message to the given target address.
*/
def send(message: Message): Unit = {
assert(message.sender == Crypto.getLocalAddress, "Message must be sent from local device")
if (!AddressDeviceMap.containsKey(message.receiver)) {
Log.w(Tag, "Receiver " + message.receiver + " is not connected, ignoring message")
def sendTo(target: Address, body: MessageBody): Unit = {
if (!AddressDeviceMap.containsKey(target)) {
Log.w(Tag, "Receiver " + target + " is not connected, ignoring message")
return
}
val header = new MessageHeader(Data.Type, MessageHeader.DefaultHopLimit,
new Date(), Crypto.getLocalAddress, message.receiver, 0, 0)
val header = new MessageHeader(body.Type, MessageHeader.DefaultHopLimit,
Crypto.getLocalAddress, target, 0, 0)
val plain = message.write(Crypto.calculateSignature(message))
val (encrypted, key) = Crypto.encrypt(message.receiver, plain)
val packer = new ScalaMessagePack().createBufferPacker()
packer
.write(encrypted)
.write(key)
val body = new Data(packer.toByteArray)
connections.apply(AddressDeviceMap.get(message.receiver)).send(header, body)
Database.addMessage(message)
val msg = new Message(header, body)
val encrypted = Crypto.encrypt(Crypto.sign(msg))
connections.apply(AddressDeviceMap.get(target)).send(encrypted)
Database.addMessage(msg)
callMessageReceivedListeners(msg)
}
/**
* Saves the message to database and sends it to registered listeners.
*
* If you want to send a new message, use [[send]].
* If you want to send a new message, use [[sendTo]].
*
* Messages must always be sent between local device and a contact.
*
* NOTE: Messages sent from the local node using [[send]] are also passed through this method.
* NOTE: Messages sent from the local node using [[sendTo]] are also passed through this method.
*/
private def onReceiveMessage(header: MessageHeader, body: MessageBody, device: Device.ID): Unit = {
assert(header.Origin != Crypto.getLocalAddress)
private def onReceiveMessage(message: Message, device: Device.ID): Unit = {
assert(message.Header.Origin != Crypto.getLocalAddress)
body match {
message.Body match {
case info: ConnectionInfo =>
if (header.Origin == Crypto.getLocalAddress)
if (message.Header.Origin == Crypto.getLocalAddress)
return
onNeighborConnected(info, device)
case data: Data =>
val up = new ScalaMessagePack().createBufferUnpacker(data.data)
val encrypted = up.readByteArray()
val key = up.readByteArray()
val (message, signature) = Message.read(Crypto.decrypt(encrypted, key))
if (!Crypto.isValidSignature(message, signature)) {
Log.i(Tag, "Dropping message with invalid signature from " + header.Origin)
case _ =>
val decrypted = Crypto.decrypt(message)
if (!Crypto.verify(decrypted)) {
Log.i(Tag, "Dropping message with invalid signature from " + message.Header.Origin)
return
}
Database.addMessage(message)
MainHandler.post(new Runnable {
override def run(): Unit = {
messageListeners.foreach(l =>
if (l.get != null)
l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message)
else
messageListeners -= l)
}
})
callMessageReceivedListeners(decrypted)
Database.addMessage(decrypted)
}
}
/**
* Calls all [[OnMessageReceivedListener]]s with the new message.
*/
private def callMessageReceivedListeners(message: Message): Unit = {
MainHandler.post(new Runnable {
override def run(): Unit = {
messageListeners.foreach(l =>
if (l.get != null)
l.apply().onMessageReceived(new TreeSet[Message]()(Message.Ordering) + message)
else
messageListeners -= l)
}
})
}
/**
* Called when a [[ConnectionInfo]] message from a new neighbor is received.
*/
private def onNeighborConnected(info: ConnectionInfo, device: Device.ID): Unit = {
val sender = Crypto.calculateAddress(info.key)
if (sender == Address.Broadcast || sender == Address.Null) {
connections(device).close()
Log.i(Tag, "Received ConnectionInfo message with invalid sender " + sender + ", ignoring")
return
}

View file

@ -22,7 +22,7 @@ class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Un
Socket.connect()
} catch {
case e: IOException =>
Log.w(Tag, "Failed to connect to " + device.toString, e)
Log.v(Tag, "Failed to connect to " + device.toString, e)
try {
Socket.close()
} catch {

View file

@ -41,6 +41,7 @@ class ListenThread(name: String, adapter: BluetoothAdapter,
}
val device: Device = new Device(socket.getRemoteDevice, true)
Log.i(Tag, "Incoming connection from " + device.toString)
onConnected(device, socket)
}
}

View file

@ -1,7 +1,6 @@
package com.nutomic.ensichat.bluetooth
import java.io._
import java.util.Date
import android.bluetooth.BluetoothSocket
import android.util.Log
@ -18,7 +17,7 @@ import com.nutomic.ensichat.messages.Crypto
* @param onReceive Called when a message was received from the other device.
*/
class TransferThread(device: Device, socket: BluetoothSocket, service: ChatService,
crypto: Crypto, onReceive: (MessageHeader, MessageBody, Device.ID) => Unit)
crypto: Crypto, onReceive: (Message, Device.ID) => Unit)
extends Thread {
private val Tag: String = "TransferThread"
@ -44,26 +43,16 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
override def run(): Unit = {
Log.i(Tag, "Starting data transfer with " + device.toString)
send(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit, new Date(), Address.Null,
Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))
send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, ConnectionInfo.HopLimit,
Address.Null, Address.Null, 0, 0), new ConnectionInfo(crypto.getLocalPublicKey))))
while (socket.isConnected) {
try {
val headerBytes = new Array[Byte](MessageHeader.Length)
InStream.read(headerBytes, 0, MessageHeader.Length)
val header = MessageHeader.read(headerBytes)
val bodyLength = (header.Length - MessageHeader.Length).toInt
if (InStream.available() > 0) {
val msg = Message.read(InStream)
val bodyBytes = new Array[Byte](bodyLength)
InStream.read(bodyBytes, 0, bodyLength)
val body =
header.MessageType match {
case ConnectionInfo.Type => ConnectionInfo.read(bodyBytes)
case Data.Type => Data.read(bodyBytes)
}
onReceive(header, body, device.Id)
onReceive(msg, device.Id)
}
} catch {
case e: RuntimeException =>
Log.i(Tag, "Received invalid message", e)
@ -73,11 +62,12 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
}
}
service.onConnectionChanged(new Device(device.bluetoothDevice, false), null)
Log.i(Tag, "Neighbor " + device + " has disconnected")
}
def send(header: MessageHeader, body: MessageBody): Unit = {
def send(msg: Message): Unit = {
try {
OutStream.write(header.write(body))
OutStream.write(msg.write)
} catch {
case e: IOException => Log.e(Tag, "Failed to write message", e)
}
@ -85,6 +75,7 @@ class TransferThread(device: Device, socket: BluetoothSocket, service: ChatServi
def close(): Unit = {
try {
Log.i(Tag, "Closing connection to " + device)
socket.close()
} catch {
case e: IOException => Log.e(Tag, "Failed to close socket", e);

View file

@ -1,7 +1,5 @@
package com.nutomic.ensichat.fragments
import java.util.Date
import android.app.ListFragment
import android.os.Bundle
import android.view.View.OnClickListener
@ -11,10 +9,9 @@ import android.widget.TextView.OnEditorActionListener
import android.widget._
import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.EnsiChatActivity
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.aodvv2.{Address, Message, Text}
import com.nutomic.ensichat.bluetooth.ChatService
import com.nutomic.ensichat.bluetooth.ChatService.OnMessageReceivedListener
import com.nutomic.ensichat.messages.{Crypto, Message, TextMessage}
import com.nutomic.ensichat.util.MessagesAdapter
import scala.collection.SortedSet
@ -43,7 +40,7 @@ class ChatFragment extends ListFragment with OnClickListener
private var listView: ListView = _
private var adapter: ArrayAdapter[TextMessage] = _
private var adapter: ArrayAdapter[Message] = _
override def onActivityCreated(savedInstanceState: Bundle): Unit = {
super.onActivityCreated(savedInstanceState)
@ -103,10 +100,8 @@ class ChatFragment extends ListFragment with OnClickListener
case R.id.send =>
val text = messageText.getText.toString.trim
if (!text.isEmpty) {
val message =
new TextMessage(Crypto.getLocalAddress(getActivity), address, new Date(), text.toString)
chatService.send(message)
adapter.add(message)
val message = new Text(text.toString)
chatService.sendTo(address, message)
messageText.getText.clear()
}
}
@ -115,9 +110,8 @@ class ChatFragment extends ListFragment with OnClickListener
* Displays new messages in UI.
*/
override def onMessageReceived(messages: SortedSet[Message]): Unit = {
messages.filter(m => m.sender == address || m.receiver == address)
.filter(_.isInstanceOf[TextMessage])
.foreach(m => adapter.add(m.asInstanceOf[TextMessage]))
messages.filter(m => Set(m.Header.Origin, m.Header.Target).contains(address))
.foreach(adapter.add)
}
/**

View file

@ -9,7 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
import android.content.Context
import android.preference.PreferenceManager
import android.util.Log
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.aodvv2._
import com.nutomic.ensichat.messages.Crypto._
import com.nutomic.ensichat.util.PRNGFixes
@ -107,6 +107,7 @@ class Crypto(Context: Context) {
*
* @throws RuntimeException If the key does not exist.
*/
@throws[RuntimeException]
def getPublicKey(address: Address): PublicKey = {
loadKey(address.toString, classOf[PublicKey])
}
@ -114,49 +115,32 @@ class Crypto(Context: Context) {
/**
* Adds a new public key for a remote device.
*
* If a key for the device already exists, nothing is done.
*
* @param address The device to which the key belongs.
* @param key The new key to add.
* @throws RuntimeException If a this key
*/
@throws[RuntimeException]
def addPublicKey(address: Address, key: PublicKey): Unit = {
if (!havePublicKey(address)) {
saveKey(address.toString, key)
} else {
Log.i(Tag, "Already have key for " + address.toString + ", not overwriting")
}
if (havePublicKey(address))
throw new RuntimeException("Already have key for " + address + ", not overwriting")
saveKey(address.toString, key)
}
/**
* Checks if the message was properly signed.
*
* This is done by signing the output of [[Message.write()]] called with an empty signature.
*
* @param message The message to verify.
* @param signature The signature that was sent
* @return True if the signature is valid.
*/
def isValidSignature(message: Message, signature: Array[Byte], key: PublicKey = null): Boolean = {
val publicKey =
if (key != null) key
else loadKey(message.sender.toString, classOf[PublicKey])
val sig = Signature.getInstance(SignAlgorithm)
sig.initVerify(publicKey)
sig.update(message.write(Array[Byte]()))
sig.verify(signature)
}
/**
* Returns a cryptographic signature for the given message (using local private key).
*
* This is done by signing the output of [[Message.write()]] called with an empty signature.
*/
def calculateSignature(message: Message): Array[Byte] = {
def sign(msg: Message): Message = {
val sig = Signature.getInstance(SignAlgorithm)
val key = loadKey(PrivateKeyAlias, classOf[PrivateKey])
sig.initSign(key)
sig.update(message.write(Array[Byte]()))
sig.sign
sig.update(msg.Body.write)
new Message(msg.Header, new CryptoData(Option(sig.sign), None), msg.Body)
}
def verify(msg: Message, key: PublicKey = null): Boolean = {
val publicKey =
if (key != null) key
else loadKey(msg.Header.Origin.toString, classOf[PublicKey])
val sig = Signature.getInstance(SignAlgorithm)
sig.initVerify(publicKey)
sig.update(msg.Body.write)
sig.verify(msg.Crypto.Signature.get)
}
/**
@ -239,50 +223,42 @@ class Crypto(Context: Context) {
*/
private def keyFolder = new File(Context.getFilesDir, "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: Address, data: Array[Byte], key: PublicKey = null):
(Array[Byte], Array[Byte]) = {
def encrypt(msg: Message, key: PublicKey = null): Message = {
assert(msg.Crypto.Signature.isDefined, "Message must be signed before encryption")
// Symmetric encryption of data
val secretKey = makeSecretKey()
val symmetricCipher = Cipher.getInstance(SymmetricCipherAlgorithm)
symmetricCipher.init(Cipher.ENCRYPT_MODE, secretKey)
val encryptedData = copyThroughCipher(symmetricCipher, data)
val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.Body.write))
// Asymmetric encryption of secret key
val publicKey =
if (key != null) key
else loadKey(receiver.toString, classOf[PublicKey])
else loadKey(msg.Header.Target.toString, classOf[PublicKey])
val asymmetricCipher = Cipher.getInstance(KeyAlgorithm)
asymmetricCipher.init(Cipher.WRAP_MODE, publicKey)
(encryptedData, asymmetricCipher.wrap(secretKey))
new Message(msg.Header,
new CryptoData(msg.Crypto.Signature, Option(asymmetricCipher.wrap(secretKey))), encrypted)
}
/**
* 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] = {
def decrypt(msg: Message): Message = {
// 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)
val key = asymmetricCipher.unwrap(msg.Crypto.Key.get, 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
symmetricCipher.init(Cipher.DECRYPT_MODE, key)
val decryped = copyThroughCipher(symmetricCipher, msg.Body.asInstanceOf[EncryptedBody].Data)
val body = msg.Header.MessageType match {
case RequestAddContact.Type => RequestAddContact.read(decryped)
case ResultAddContact.Type => ResultAddContact.read(decryped)
case Text.Type => Text.read(decryped)
}
new Message(msg.Header, msg.Crypto, body)
}
/**

View file

@ -1,122 +0,0 @@
package com.nutomic.ensichat.messages
import java.io.IOException
import java.util.{Date, Objects}
import com.nutomic.ensichat.aodvv2.Address
import org.msgpack.ScalaMessagePack
import org.msgpack.packer.Packer
object Message {
/**
* Types of messages that can be transfered.
*
* There must be one type for each implementation and vice versa.
*/
object Type {
val Text = 1
val RequestAddContact = 2
val ResultAddContact = 3
}
/**
* Orders messages by date, oldest messages first.
*/
val Ordering = new Ordering[Message] {
override def compare(m1: Message, m2: Message) = m1.date.compareTo(m2.date)
}
/**
* Reads a byte array that was written by [[Message.write]] into the correct
* message implementation..
*
* @return Deserialized message and sits signature.
*/
def read(bytes: Array[Byte]): (Message, Array[Byte]) = {
val up = new ScalaMessagePack().createBufferUnpacker(bytes)
@throws[IOException]("If the message can't be parsed")
@throws[RuntimeException]("If the message has an unknown type")
val messageType = up.readInt()
val sender = new Address(up.readByteArray())
val receiver = new Address(up.readByteArray())
val date = new Date(up.readLong())
val sig = up.readByteArray()
(messageType match {
case Type.Text => TextMessage.read(sender, receiver, date, up)
case Type.RequestAddContact => RequestAddContactMessage.read(sender, receiver, date, up)
case Type.ResultAddContact => ResultAddContactMessage.read(sender, receiver, date, up)
case t =>
throw new RuntimeException("Received message of unknown type " + t)
}, sig)
}
}
/**
* Message object that can be sent between remote devices.
*
* @param messageType One of [[Message.Type]].
*/
@Deprecated
abstract class Message(val messageType: Int) {
/**
* Device where the message was sent from.
*/
val sender: Address
/**
* Device the message is addressed to.
*/
val receiver: Address
/**
* Timestamp of message creation.
*/
val date: Date
/**
* Writes this message and the given signature into byte array.
*
* Signature may not be null, but can be an empty array.
*/
def write(signature: Array[Byte]): Array[Byte] = {
val packer = new ScalaMessagePack().createBufferPacker()
packer.write(messageType)
.write(sender.Bytes)
.write(receiver.Bytes)
.write(date.getTime)
.write(signature)
doWrite(packer)
packer.toByteArray
}
/**
* Serializes any extra data for implementing classes.
*/
protected def doWrite(packer: Packer): Unit
/**
* Returns true if objects are equal.
*
* Implementations must provide their own implementation to check the result of this
* function and their own data.
*/
override def equals(a: Any): Boolean = a match {
case o: Message => sender == o.sender && receiver == o.receiver && date == o.date
case _ => false
}
/**
* Returns a hash code for this object.
*
* Implementations must provide their own implementation to check the result of this
* function and their own data.
*/
override def hashCode: Int = Objects.hash(sender, receiver, date)
override def toString: String
}

View file

@ -1,30 +0,0 @@
package com.nutomic.ensichat.messages
import java.util.Date
import com.nutomic.ensichat.activities.AddContactsActivity
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.messages.Message._
import org.msgpack.packer.Packer
import org.msgpack.unpacker.Unpacker
object RequestAddContactMessage {
def read(sender: Address, receiver: Address, date: Date, up: Unpacker) =
new RequestAddContactMessage(sender, receiver, date)
}
/**
* Message sent by [[AddContactsActivity]] to notify a device that it should be added as a contact.
*/
class RequestAddContactMessage(override val sender: Address, override val receiver: Address,
override val date: Date) extends Message(Type.RequestAddContact) {
override def doWrite(packer: Packer) = {
}
override def toString = "RequestAddContactMessage(" + sender.toString + ", " + receiver.toString +
", " + date.toString + ")"
}

View file

@ -1,37 +0,0 @@
package com.nutomic.ensichat.messages
import java.util.{Date, Objects}
import com.nutomic.ensichat.activities.AddContactsActivity
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.messages.Message._
import org.msgpack.packer.Packer
import org.msgpack.unpacker.Unpacker
object ResultAddContactMessage {
def read(sender: Address, receiver: Address, date: Date, up: Unpacker) =
new ResultAddContactMessage(sender, receiver, date, up.readBoolean())
}
/**
* Message sent by [[AddContactsActivity]] to tell a device whether the user confirmed adding it
* to contacts.
*/
class ResultAddContactMessage(override val sender: Address, override val receiver: Address,
override val date: Date, val Accepted: Boolean)
extends Message(Type.ResultAddContact) {
override def doWrite(packer: Packer) = packer.write(Accepted)
override def equals(a: Any) =
super.equals(a) && a.asInstanceOf[ResultAddContactMessage].Accepted == Accepted
override def hashCode =
Objects.hash(super.hashCode: java.lang.Integer, Accepted: java.lang.Boolean)
override def toString = "ResultAddContactMessage(" + sender.toString + ", " + receiver.toString +
", " + date.toString + ", " + Accepted + ")"
}

View file

@ -1,32 +0,0 @@
package com.nutomic.ensichat.messages
import java.util.{Date, Objects}
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.messages.Message._
import org.msgpack.packer.Packer
import org.msgpack.unpacker.Unpacker
object TextMessage {
def read(sender: Address, receiver: Address, date: Date, up: Unpacker): TextMessage =
new TextMessage(sender, receiver, date, up.readString())
}
/**
* Message that contains text.
*/
class TextMessage(override val sender: Address, override val receiver: Address,
override val date: Date, val text: String) extends Message(Type.Text) {
override def doWrite(packer: Packer) = packer.write(text)
override def equals(a: Any) = super.equals(a) && a.asInstanceOf[TextMessage].text == text
override def hashCode = Objects.hash(super.hashCode: java.lang.Integer, text)
override def toString = "TextMessage(" + sender.toString + ", " + receiver.toString +
", " + date.toString + ", " + text + ")"
}

View file

@ -2,6 +2,9 @@ package com.nutomic.ensichat.util
import java.nio.ByteBuffer
/**
* Provides various helper methods for [[ByteBuffer]].
*/
object BufferUtils {
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
@ -25,6 +28,6 @@ object BufferUtils {
b
}
def toString(bb: ByteBuffer)= bb.array().slice(0, 4).map("%02X" format _).mkString
def toString(array: Array[Byte]) = array.map("%02X".format(_)).mkString
}

View file

@ -4,8 +4,7 @@ import java.util.Date
import android.content.{ContentValues, Context}
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.messages._
import com.nutomic.ensichat.aodvv2._
import scala.collection.SortedSet
import scala.collection.immutable.TreeSet
@ -18,8 +17,8 @@ object Database {
private val CreateMessagesTable = "CREATE TABLE messages(" +
"_id integer primary key autoincrement," +
"sender text not null," +
"receiver text not null," +
"origin text not null," +
"target text not null," +
"text text not null," +
"date integer not null);" // Unix timestamp of message.
@ -35,8 +34,6 @@ object Database {
class Database(context: Context) extends SQLiteOpenHelper(context, Database.DatabaseName,
null, Database.DatabaseVersion) {
private val Tag = "MessageStore"
private var contactsUpdatedListeners = Set[() => Unit]()
override def onCreate(db: SQLiteDatabase): Unit = {
@ -49,17 +46,21 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
*/
def getMessages(address: Address, count: Int): SortedSet[Message] = {
val c = getReadableDatabase.query(true,
"messages", Array("sender", "receiver", "text", "date"),
"sender = ? OR receiver = ?", Array(address.toString, address.toString),
"messages", Array("origin", "target", "text", "date"),
"origin = ? OR target = ?", Array(address.toString, address.toString),
null, null, "date DESC", count.toString)
var messages = new TreeSet[Message]()(Message.Ordering)
while (c.moveToNext()) {
val m = new TextMessage(
new Address(c.getString(c.getColumnIndex("sender"))),
new Address(c.getString(c.getColumnIndex("receiver"))),
new Date(c.getLong(c.getColumnIndex("date"))),
new String(c.getString(c.getColumnIndex ("text"))))
messages += m
val header = new MessageHeader(
Text.Type,
-1,
new Address(c.getString(c.getColumnIndex("origin"))),
new Address(c.getString(c.getColumnIndex("target"))),
-1,
-1,
new Date(c.getLong(c.getColumnIndex("date"))))
val body = new Text(new String(c.getString(c.getColumnIndex ("text"))))
messages += new Message(header, body)
}
c.close()
messages
@ -68,16 +69,16 @@ class Database(context: Context) extends SQLiteOpenHelper(context, Database.Data
/**
* Inserts the given new message into the database.
*/
def addMessage(message: Message): Unit = message match {
case msg: TextMessage =>
def addMessage(message: Message): Unit = message.Body match {
case msg: Text =>
val cv = new ContentValues()
cv.put("sender", msg.sender.toString)
cv.put("receiver", msg.receiver.toString)
cv.put("origin", message.Header.Origin.toString)
cv.put("target", message.Header.Target.toString)
// toString used as workaround for compile error with Long.
cv.put("date", msg.date.getTime.toString)
cv.put("date", message.Header.Time.getTime.toString)
cv.put("text", msg.text)
getWritableDatabase.insert("messages", null, cv)
case _: RequestAddContactMessage | _: ResultAddContactMessage =>
case _: ConnectionInfo | _: RequestAddContact | _: ResultAddContact =>
// Never stored.
}

View file

@ -4,14 +4,13 @@ import android.content.Context
import android.view.{Gravity, View, ViewGroup}
import android.widget.{ArrayAdapter, RelativeLayout, TextView}
import com.nutomic.ensichat.R
import com.nutomic.ensichat.aodvv2.Address
import com.nutomic.ensichat.messages.TextMessage
import com.nutomic.ensichat.aodvv2.{Address, Message, Text}
/**
* Displays [[TextMessage]]s in ListView.
* Displays [[Message]]s in ListView.
*/
class MessagesAdapter(context: Context, remoteAddress: Address) extends
ArrayAdapter[TextMessage](context, R.layout.item_message, android.R.id.text1) {
ArrayAdapter[Message](context, R.layout.item_message, android.R.id.text1) {
/**
* Free space to the right/left to a message depending on who sent it, in dip.
@ -22,11 +21,11 @@ class MessagesAdapter(context: Context, remoteAddress: Address) extends
val view = super.getView(position, convertView, parent).asInstanceOf[RelativeLayout]
val tv = view.findViewById(android.R.id.text1).asInstanceOf[TextView]
tv.setText(getItem(position).text)
tv.setText(getItem(position).Body.asInstanceOf[Text].text)
val lp = new RelativeLayout.LayoutParams(tv.getLayoutParams)
val margin = (MessageMargin * context.getResources.getDisplayMetrics.density).toInt
if (getItem(position).sender != remoteAddress) {
if (getItem(position).Header.Origin != remoteAddress) {
view.setGravity(Gravity.RIGHT)
lp.setMargins(margin, 0, 0, 0)
} else {

View file

@ -7,7 +7,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:0.14.2'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.5'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.6'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

BIN
gradle/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Thu Dec 04 22:57:09 EET 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip

164
gradle/gradlew vendored Executable file
View file

@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
gradle/gradlew.bat vendored Normal file
View file

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

11
gradle/local.properties Normal file
View file

@ -0,0 +1,11 @@
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Dec 04 22:57:09 EET 2014
sdk.dir=/home/felix/software/android-sdk

Binary file not shown.

View file

@ -1,5 +1,6 @@
#Tue Dec 09 17:41:26 EET 2014
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip