restful push and pop with cryptography
- integration test pushing and poping a message and comparing decrypted strings passes - integration test comparing encrypted and decrypted string passes
This commit is contained in:
parent
10ba4cf31b
commit
c46c664cdb
@ -1,108 +0,0 @@
|
|||||||
package com.maenle.bump
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import com.example.bump.MessageProcessor
|
|
||||||
import com.example.bump.RestSingleton
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("com.maenle.bump", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class MessageProcessorTest {
|
|
||||||
@Test
|
|
||||||
fun decryptMessages() {
|
|
||||||
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
|
||||||
val messageRaw = "M1dEAxKZ5HUHCJoRkgGOvAABhqCAAAAAAGG2eKTSlKXWLDQx5B_wssZsNwsanzQID2UyUm4KKuKYKgfwH5MG2N-qzt6K4mg3pfZmWPaiDB9PiqlX236k6zo9Yvvq"
|
|
||||||
val data = decryptMessage(code, messageRaw)
|
|
||||||
assertEquals(data, "hello")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun decryptMessage(code: String, messageRaw: String): String {
|
|
||||||
val message = MessageProcessor(code)
|
|
||||||
val data = message.decrypt(messageRaw)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class RestTest{
|
|
||||||
@Test
|
|
||||||
fun getPeek() {
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
val sender = "dydoes-unknowledgeable-indiscretion-househusbands"
|
|
||||||
RestSingleton.getInstance(context).peek(sender) { i -> assertEquals(i, "hello") }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class IntegrationTest{
|
|
||||||
@Test
|
|
||||||
fun pushPopAndDecrypt() {
|
|
||||||
val testMessage = "Hi There Sir"
|
|
||||||
val sender = "dydoes-unknowledgeable-indiscretion-househusbands"
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
val lock = CountDownLatch(1);
|
|
||||||
|
|
||||||
fun messageTester(encrypted: String) {
|
|
||||||
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
|
||||||
Log.d("TEST", encrypted)
|
|
||||||
val message = MessageProcessor(code)
|
|
||||||
val data = message.decrypt(encrypted)
|
|
||||||
Log.d("TEST", "data")
|
|
||||||
assertEquals(data, testMessage)
|
|
||||||
lock.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
RestSingleton.getInstance(context).push(sender, testMessage) {
|
|
||||||
RestSingleton.getInstance(context).pop(sender) { i -> messageTester(i) }
|
|
||||||
}
|
|
||||||
Log.d("TEST", "waiting")
|
|
||||||
lock.await(20000, TimeUnit.MILLISECONDS)
|
|
||||||
Log.d("TEST", "done")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun popAndDectypt() {
|
|
||||||
val testMessage = "Hi There Sir"
|
|
||||||
val sender = "raphael"
|
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
val lock = CountDownLatch(1);
|
|
||||||
|
|
||||||
|
|
||||||
fun messageTester(encrypted: String) {
|
|
||||||
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
|
||||||
val message = MessageProcessor(code)
|
|
||||||
val data = message.decrypt(encrypted)
|
|
||||||
assertEquals(data, testMessage)
|
|
||||||
lock.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
RestSingleton.getInstance(context).pop(sender) { i -> messageTester(i) }
|
|
||||||
lock.await(20000, TimeUnit.MILLISECONDS)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
package com.example.bump
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class MessageProcessorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decryptMessage() {
|
||||||
|
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
||||||
|
// val messageRaw = "M1dEAxKZ5HUHCJoRkgGOvAABhqCAAAAAAGG2eKTSlKXWLDQx5B_wssZsNwsanzQID2UyUm4KKuKYKgfwH5MG2N-qzt6K4mg3pfZmWPaiDB9PiqlX236k6zo9Yvvq"
|
||||||
|
val messageRaw = "M1dEAxKZ5HUHCJoRkgGOvAABhqCAAAAAAGG8afPPk380EzwcbGzNoTr_I4y6YT8hnUYcToinlgsVkaUx5K-JicdS5epZenOX4u8vVhhMvR0ebeWm_mgp6LZvTw8S"
|
||||||
|
val data = decryptMessage(code, messageRaw)
|
||||||
|
Assert.assertEquals(data, "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun encryptAndDecryptMessage() {
|
||||||
|
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
||||||
|
val message = MessageProcessor(code)
|
||||||
|
|
||||||
|
val test = getRandomString(32)
|
||||||
|
val encrypted = message.encrypt(test)
|
||||||
|
val decrypted = decryptMessage(code, encrypted)
|
||||||
|
Assert.assertEquals(test, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun decryptMessage(code: String, messageData: String): String {
|
||||||
|
val message = MessageProcessor(code)
|
||||||
|
val data = message.decrypt(messageData)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
app/src/androidTest/java/com/example/bump/RestCryptTest.kt
Normal file
69
app/src/androidTest/java/com/example/bump/RestCryptTest.kt
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package com.example.bump
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.maenle.bump", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RestCryptTest{
|
||||||
|
@Test
|
||||||
|
fun pushPopAndDecrypt() {
|
||||||
|
val code = "dydoes-unknowledgeable-indiscretion-househusbands-pot-walloper-indiscretion-discophorous-transcriptions-dydoes-poodle-faker-transcriptions-budlike"
|
||||||
|
val testMessage = getRandomString(32)
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
var encrypted = ""
|
||||||
|
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
|
||||||
|
|
||||||
|
fun messageTester(messageEncrypted: String) {
|
||||||
|
val message = MessageProcessor(code)
|
||||||
|
|
||||||
|
val messageData: String = JSONObject(messageEncrypted).get("data").toString()
|
||||||
|
assertEquals(messageData, encrypted)
|
||||||
|
val data = message.decrypt(messageData)
|
||||||
|
assertEquals(data, testMessage)
|
||||||
|
lock.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
val message = MessageProcessor(code)
|
||||||
|
encrypted = message.encrypt(testMessage)
|
||||||
|
RestSingleton.getInstance(context).push(message.sender, encrypted) {
|
||||||
|
RestSingleton.getInstance(context).pop(message.sender) { i -> messageTester(i) }
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.await(200000, TimeUnit.MILLISECONDS)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRandomString(length: Int) : String {
|
||||||
|
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + (' ') + ('_') + ('-')
|
||||||
|
return (1..length)
|
||||||
|
.map { allowedChars.random() }
|
||||||
|
.joinToString("")
|
||||||
|
}
|
@ -9,11 +9,12 @@ import com.macasaet.fernet.StringValidator
|
|||||||
import com.macasaet.fernet.Validator
|
import com.macasaet.fernet.Validator
|
||||||
import com.maenle.bump.MainActivity
|
import com.maenle.bump.MainActivity
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
import java.security.SecureRandom
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.temporal.TemporalAmount
|
import java.time.temporal.TemporalAmount
|
||||||
|
|
||||||
class MessageProcessor(code: String) {
|
class MessageProcessor(code: String, private val salt: ByteArray? = null) {
|
||||||
private var sender: String
|
var sender: String
|
||||||
private var password: String
|
private var password: String
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -28,30 +29,61 @@ class MessageProcessor(code: String) {
|
|||||||
return code.split("-").size >= KEY_LENGTH + SENDER_LENGTH
|
return code.split("-").size >= KEY_LENGTH + SENDER_LENGTH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun encrypt(decrypted: String): String {
|
||||||
|
val message = MessageEncrypt(decrypted)
|
||||||
|
salt?.let { message.updateSalt(salt) }
|
||||||
|
return message.encryptWith(password)
|
||||||
|
}
|
||||||
|
|
||||||
fun decrypt(messageRaw: String): String {
|
fun decrypt(messageRaw: String): String {
|
||||||
val message = Message(messageRaw)
|
val message = MessageDecrypt(messageRaw)
|
||||||
val fernetKey: Key = message.deriveMessageKeyFromPassword(password)
|
return message.validateAndDecryptWith(password)
|
||||||
return message.validateAndDecrypt(fernetKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class Message(messageRaw : String) {
|
private class MessageEncrypt(val messageDecrypted: String) : Message() {
|
||||||
lateinit var salt : ByteArray
|
fun encryptWith(password: String): String {
|
||||||
var iterations: Int = 100_00
|
val fernetKey = deriveMessageKeyFromPassword(password)
|
||||||
lateinit var token : Token
|
val token = Token.generate(fernetKey, messageDecrypted)
|
||||||
|
val tokenString = Base64.getUrlDecoder().decode(token.serialise())
|
||||||
|
|
||||||
init {
|
val iterationBuffer = ByteArray(4)
|
||||||
splitMessage(messageRaw)
|
for (i in 0..3) iterationBuffer[i] = (iterations shr (i*8)).toByte()
|
||||||
|
iterationBuffer.reverse()
|
||||||
|
|
||||||
|
val data = salt + iterationBuffer + tokenString
|
||||||
|
return String(Base64.getUrlEncoder().encode(data))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun splitMessage(message : String) {
|
|
||||||
|
private class MessageDecrypt(messageRaw: String) : Message() {
|
||||||
|
init {
|
||||||
|
super.splitMessage(messageRaw)
|
||||||
|
}
|
||||||
|
fun validateAndDecryptWith(password: String, timeToLive: Long = TIME_TO_LIVE): String {
|
||||||
|
val fernetKey = deriveMessageKeyFromPassword(password)
|
||||||
|
val validator: Validator<String> = object : StringValidator {
|
||||||
|
override fun getTimeToLive(): TemporalAmount {
|
||||||
|
return Duration.ofHours(timeToLive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token.validateAndDecrypt(fernetKey, validator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
abstract class Message(var salt: ByteArray = freshSalt, var iterations: Int = ITERATIONS) {
|
||||||
|
lateinit var token: Token
|
||||||
|
|
||||||
|
protected fun splitMessage(message : String) {
|
||||||
val decodedMessage : ByteArray = Base64.getUrlDecoder().decode(message)
|
val decodedMessage : ByteArray = Base64.getUrlDecoder().decode(message)
|
||||||
|
|
||||||
|
salt = decodedMessage.copyOfRange(0, SALT_LENGTH)
|
||||||
val iterationsDecoded = decodedMessage.copyOfRange(SALT_LENGTH, SALT_LENGTH+ ITERATIONS_LENGTH)
|
val iterationsDecoded = decodedMessage.copyOfRange(SALT_LENGTH, SALT_LENGTH+ ITERATIONS_LENGTH)
|
||||||
val tokenString = String(Base64.getUrlEncoder().encode(decodedMessage.copyOfRange(20, decodedMessage.size)))
|
val tokenString = String(Base64.getUrlEncoder().encode(decodedMessage.copyOfRange(20, decodedMessage.size)))
|
||||||
|
|
||||||
iterations = BigInteger(iterationsDecoded).toInt()
|
iterations = BigInteger(iterationsDecoded).toInt()
|
||||||
salt = decodedMessage.copyOfRange(0, SALT_LENGTH)
|
|
||||||
token = Token.fromString(tokenString)
|
token = Token.fromString(tokenString)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +107,24 @@ class MessageProcessor(code: String) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deriveMessageKeyFromPassword(password: String): Key{
|
fun updateSalt(newSalt: ByteArray) {
|
||||||
|
|
||||||
|
if(newSalt.size == SALT_LENGTH) {
|
||||||
|
salt = newSalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val freshSalt: ByteArray
|
||||||
|
get()
|
||||||
|
{
|
||||||
|
val salt = ByteArray(16)
|
||||||
|
SecureRandom().nextBytes(salt)
|
||||||
|
return salt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deriveMessageKeyFromPassword(password: String): Key {
|
||||||
val derivedKeyLength = 256
|
val derivedKeyLength = 256
|
||||||
val spec = PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength)
|
val spec = PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength)
|
||||||
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||||
@ -84,25 +133,19 @@ class MessageProcessor(code: String) {
|
|||||||
|
|
||||||
return Key(saltedKey)
|
return Key(saltedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateAndDecrypt(fernetKey: Key, timeToLive: Long = 24*365*1000): String {
|
|
||||||
|
|
||||||
val validator: Validator<String> = object : StringValidator {
|
|
||||||
override fun getTimeToLive(): TemporalAmount {
|
|
||||||
return Duration.ofHours(timeToLive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return token.validateAndDecrypt(fernetKey, validator)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = MainActivity::class.java.simpleName
|
private val TAG = MainActivity::class.java.simpleName
|
||||||
const val KEY_LENGTH = 8
|
const val KEY_LENGTH: Int = 8
|
||||||
const val SENDER_LENGTH = 4
|
const val SENDER_LENGTH: Int = 4
|
||||||
const val SALT_LENGTH = 16
|
const val SALT_LENGTH: Int = 16
|
||||||
const val ITERATIONS_LENGTH = 4
|
const val ITERATIONS_LENGTH: Int = 4
|
||||||
|
|
||||||
|
const val ITERATIONS: Int = 100_000
|
||||||
|
const val TIME_TO_LIVE: Long = 1000 * 365 * 24
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user