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:
Raphael Maenle 2021-12-17 12:15:54 +01:00
parent 10ba4cf31b
commit c46c664cdb
4 changed files with 179 additions and 136 deletions

View File

@ -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)
}
}

View File

@ -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
}
}
}

View 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("")
}

View File

@ -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
} }