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:
parent
de86f5f121
commit
834f3ca724
43 changed files with 984 additions and 585 deletions
112
PROTOCOL.md
112
PROTOCOL.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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 _ =>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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) + ")"
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala
Normal file
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Message.scala
Normal 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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala
Normal file
50
app/src/main/scala/com/nutomic/ensichat/aodvv2/Text.scala
Normal 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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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 + ")"
|
||||
|
||||
}
|
|
@ -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 + ")"
|
||||
|
||||
}
|
|
@ -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 + ")"
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
BIN
gradle/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
164
gradle/gradlew
vendored
Executable 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
90
gradle/gradlew.bat
vendored
Normal 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
11
gradle/local.properties
Normal 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
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
Reference in a new issue