From b1b4ad30302600ad39959c8039cf12ba072b61d3 Mon Sep 17 00:00:00 2001 From: Mondei1 Date: Fri, 3 Jul 2020 16:28:26 +0200 Subject: [PATCH] Add compression support --- app/build.gradle | 8 +- .../github/mondei1/offpass/CreateActivity.kt | 8 +- .../mondei1/offpass/CryptoOperations.kt | 136 ++++++++++++++++++ .../github/mondei1/offpass/MainActivity.kt | 28 ++++ .../com/github/mondei1/offpass/QRSchema.kt | 73 ++++++++-- .../mondei1/offpass/entities/Compression.kt | 13 ++ .../offpass/entities/CompressionHelper.kt | 89 ++++++++++++ 7 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt create mode 100644 app/src/main/java/com/github/mondei1/offpass/entities/Compression.kt create mode 100644 app/src/main/java/com/github/mondei1/offpass/entities/CompressionHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 4cdf962..3415a72 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,13 +35,19 @@ repositories { dependencies { + def room_version = "2.2.5" + implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.0' + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" + + / QRCode reader / implementation 'com.journeyapps:zxing-android-embedded:4.1.0' + implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.wear:wear:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/app/src/main/java/com/github/mondei1/offpass/CreateActivity.kt b/app/src/main/java/com/github/mondei1/offpass/CreateActivity.kt index 563cd24..171be4f 100644 --- a/app/src/main/java/com/github/mondei1/offpass/CreateActivity.kt +++ b/app/src/main/java/com/github/mondei1/offpass/CreateActivity.kt @@ -3,6 +3,8 @@ package com.github.mondei1.offpass import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity +import com.github.mondei1.offpass.entities.Compression +import com.github.mondei1.offpass.entities.CompressionHelper import kotlinx.android.synthetic.main.activity_create.* class CreateActivity : AppCompatActivity() { @@ -20,9 +22,9 @@ class CreateActivity : AppCompatActivity() { .replace(R.id.username, fragment_username!!) .commit() - this.schema = QRSchema() - this.schema!!.decrypted_raw = "%JtuB4O9M42%Gitea|Nicolas|542superGoOD_pW&|klier.nicolas@protonmail.com|https://nicolasklier.de:3000|()What's your favorite series%Rick and morty|(2fa)otpauth://totp/OffPass%20Test?secret=d34gfkki5dkd5knifysrpgndd5xb2c7eddwki7ya4pvoisfa5c3ko5pv&issuer=Nicolas%20Klier" - this.schema!!.parse() + this.schema = QRSchema(this) + this.schema!!.decrypted_raw = "%JtuB4O9M42%Gitea|Nicolas|542superGoOD_pW&|klier.nicolas@protonmail.com|\$ul|(\$vb)\$O4|()What's your favorite series%Rick and morty|(2fa)otpauth://totp/OffPass%20Test?secret=d34gfkki5dkd5knifysrpgndd5xb2c7eddwki7ya4pvoisfa5c3ko5pv&issuer=Nicolas%20Klier" + this.schema!!.parse(this) setSupportActionBar(findViewById(R.id.toolbar)) diff --git a/app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt b/app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt new file mode 100644 index 0000000..5db7563 --- /dev/null +++ b/app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt @@ -0,0 +1,136 @@ +package com.github.mondei1.offpass + +import android.os.HandlerThread +import android.os.Looper +import android.util.Base64 +import android.util.Log +import java.nio.ByteBuffer +import java.security.SecureRandom +import java.security.spec.KeySpec +import java.util.concurrent.CountDownLatch +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +class EncryptionResult( + val result: String = "", + val salt: ByteArray = ByteArray(1), + val iv: ByteArray = ByteArray(1) +) { +} + +class CryptoOperations { + private val random: SecureRandom = SecureRandom() + private val possibleChars: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + private lateinit var chars: CharArray + + /** + * Hash a password for storage. + * + * @return a secure authentication token to be stored for later authentication + */ + fun hash(password: CharArray, salt: ByteArray): String { + val spec: KeySpec = PBEKeySpec(password, salt, 85000, 8*31) + val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") + val hash = factory.generateSecret(spec).encoded + + Log.i("Crypto", "Hashed ${password.toString()} to ${Base64.encodeToString(hash, Base64.DEFAULT)}") + return Base64.encodeToString(hash, Base64.DEFAULT) + } + + fun encrypt(key: String, plain: String): EncryptionResult { + val salt = nextString(10).toByteArray() + val iv = nextString(16).toByteArray() + val key_bytes: ByteArray = hash(key.toCharArray(), salt).toByteArray(Charsets.UTF_8) + val aes_key: ByteArray = ByteArray(32) + + // Now make the key bytes fit into 256 bits + if (key_bytes.size < aes_key.size) { + throw Error("Tried to cut down hash bytes to 32 but there are less then that.") + } + for (i in aes_key.indices) { + aes_key.set(i, key_bytes[i]) + } + + val key: SecretKey = SecretKeySpec(aes_key, "AES") + val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING") + + // Performing actual crypto operation + cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv)) + Log.i("Crypto", "Encrypt using ${String(aes_key)} '$plain' ${String(iv)}") + val ciphered = cipher.doFinal(plain.toByteArray(Charsets.UTF_8)) + + // Concat everything into one byte array + val byteBuffer: ByteBuffer = ByteBuffer.allocate(ciphered.size) + byteBuffer.put(ciphered) + + Log.d("Crypto", "Encrypted $plain => ${String(byteBuffer.array())}") + return EncryptionResult(Base64.encodeToString(byteBuffer.array(), Base64.DEFAULT), salt, iv) + } + + fun decrypt(encrypted: String, key: String, iv: ByteArray, salt: ByteArray): String? { + val key_bytes: ByteArray = hash(key.toCharArray(), salt).toByteArray(Charsets.UTF_8) + val aes_key: ByteArray = ByteArray(32) + + // Now make the key bytes fit into 256 + if (key_bytes.size < aes_key.size) { + throw Error("Tried to cut down hash bytes to 32 but there are less then that.") + } + for (i in 0..aes_key.size-1) { + aes_key.set(i, key_bytes[i]) + } + + // Decode Base64 + val raw_encrypted = Base64.decode(encrypted, Base64.DEFAULT) + + Log.i("Crypto", "Decrypt using ${String(aes_key)} '${String(raw_encrypted)}' ${String(iv)}") + + val key: SecretKey = SecretKeySpec(aes_key, "AES") + val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING") + + // Performing actual crypto operation + cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) + val ciphered = cipher.doFinal(raw_encrypted) + + // Concat everything into one byte array + val byteBuffer: ByteBuffer = ByteBuffer.allocate(ciphered.size) + byteBuffer.put(ciphered) + + Log.d("Crypto", "Decrypted $encrypted => ${String(byteBuffer.array())}") + return String(byteBuffer.array()) + } + + fun nextIV(): ByteArray { + val iv: ByteArray = ByteArray(12) + val random: SecureRandom = SecureRandom() + random.nextBytes(iv) + + return iv + } + + fun nextString(length: Int): String { + chars = CharArray(length) + for (idx in chars.indices) chars[idx] = possibleChars[random.nextInt(possibleChars.length)] + Log.i("Random", "Produced random string: ${String(chars)}") + + return String(chars) + } + +} + +class CryptoEncryptionRunnable( + private val key: String, + private val data: String, + private val iv: ByteArray +) : HandlerThread("Enc_Crypto") { + + override fun run() { + Looper.prepare() + val crypto = CryptoOperations() + crypto.hash(key.toCharArray(), iv) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt b/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt index 57ee409..21ac6cb 100644 --- a/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt +++ b/app/src/main/java/com/github/mondei1/offpass/MainActivity.kt @@ -2,12 +2,18 @@ package com.github.mondei1.offpass import android.content.Intent import android.os.Bundle +import android.os.HandlerThread import android.util.Log import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import com.github.mondei1.offpass.entities.Compression +import com.github.mondei1.offpass.entities.CompressionHelper import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentResult import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking class MainActivity : AppCompatActivity() { @@ -17,6 +23,28 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) + var dbHandler = CompressionHelper(this, null) + dbHandler.writableDatabase.execSQL("DELETE FROM `${CompressionHelper.TABLE_NAME}`;") + + var dummy_email = Compression("ul", "https://nicolasklier.de:3000", null) + var dummy_custom_key = Compression("vb", "Note", null) + var dummy_custom_value = Compression("O4", "This is a small note I have to keep.", null) + + //CryptoOperations().hash("secretpassword".toCharArray(), CryptoOperations().nextIV()) + val cryptoThread = HandlerThread("Encryption thread") + cryptoThread.start() + + val encrypted_result = GlobalScope.async { + dbHandler.add("JtuB4O9M42", dummy_email) + dbHandler.add("JtuB4O9M42", dummy_custom_key) + dbHandler.add("JtuB4O9M42", dummy_custom_value) + + Log.i("Main", "Encrypted target data to: ${CryptoOperations().encrypt("My cool key", "my secret")}") + + } + + dbHandler.writableDatabase.close() + main_screen.setOnClickListener { IntentIntegrator(this) .setPrompt("Scan OffPass created QR-Code") diff --git a/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt b/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt index 638be08..19a7c09 100644 --- a/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt +++ b/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt @@ -1,12 +1,19 @@ package com.github.mondei1.offpass +import android.content.Context import android.util.Log +import com.github.mondei1.offpass.entities.Compression +import com.github.mondei1.offpass.entities.CompressionHelper +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.broadcast +import kotlinx.coroutines.runBlocking import java.lang.Error -import java.util.regex.Matcher -import java.util.regex.Pattern class QRSchema { + private var db: CompressionHelper? = null + var raw: String = "" var decrypted_raw: String = "" @@ -20,14 +27,34 @@ class QRSchema { var custom: HashMap = HashMap() // All defined custom/optional fields var question_awnser: HashMap = HashMap() // Used for security questions - constructor() {} + constructor(context: Context) { + this.db = CompressionHelper(context, null) + } + + /** + * This function takes a key `db_key` and tries to resolve it. + */ + private suspend fun resolve(key: String, context: Context): String { + val to_resolve = key.replace("$", "") + Log.i("QR-Code schema", "Try to resolve $to_resolve") + val database = db + + return GlobalScope.async { + var cursor = database!!.get(session_key, to_resolve) + if (cursor != null) { + cursor.toString() + } else { + "" + } + }.await() + } /** * This function will take the already set raw content and tries to parse it. * * @return It will return `true` if the operation was successful and `false` if otherwise. */ - fun parse(): Boolean { + fun parse(context: Context): Boolean { if (decrypted_raw == "") { throw Error("Tried to parse QR-Code schema but raw content must be first decrypted! " + "Set raw content first and then use decrypt()") @@ -42,9 +69,9 @@ class QRSchema { // Check if there is a session key if (fields[0].startsWith("%")) { fields[0] = fields[0].replace("%", "") - session_key = fields[0].substring(0, 9) // Get first 10 chars, which are the + session_key = fields[0].substring(0, 10) // Get first 10 chars, which are the // session key - this.title = fields[0].substring(10, fields[0].length) + this.title = fields[0].substring(11, fields[0].length) } else { this.title = fields[0] } @@ -53,15 +80,35 @@ class QRSchema { this.password = fields[2] this.email = fields[3] this.website_url = fields[4] + + // Look if website url is compressed + if (this.website_url.startsWith("$")) { + var that_class = this + runBlocking { + that_class.website_url = resolve(that_class.website_url, context) + } + } } // Here are optional/custom fields if (i > 4) { - if (fields[i].startsWith("(")) { + if (fields[i].startsWith("(") && fields[i] != "") { var closingBracket = fields[i].indexOf(")") var key = fields[i].substring(1, closingBracket) var value = fields[i].substring(closingBracket+1, fields[i].length) + // Check if key and or value are compressed + if (key.startsWith("$")) { + runBlocking { + key = resolve(key, context) + } + } + if (value.startsWith("$")) { + runBlocking { + value = resolve(value, context) + } + } + // We got a security question/awnser if (key == "") { var qa = value.split("%") @@ -69,15 +116,23 @@ class QRSchema { } else { custom.put(key, value) } - Log.i("QR-Code schema", custom.toString()) } else { throw Error("Custom/optional field should start with an open bracket: " + fields[i].toString()) } } + + // Final step is to resolve any compressed values } - Log.i("QR-Code schema", "Found: $session_key, $title, $username, $password, $email, $website_url. ${custom.toString()} and ${question_awnser.toString()}") + Log.i("QR-Code schema", "Found: $session_key, $title, $username, $password, " + + "$email, $website_url. ${custom.toString()} and ${question_awnser.toString()}") return true + } + + /** + * This function will take all defined variables and converts them into the QR-Code schema + */ + fun build() { } diff --git a/app/src/main/java/com/github/mondei1/offpass/entities/Compression.kt b/app/src/main/java/com/github/mondei1/offpass/entities/Compression.kt new file mode 100644 index 0000000..ea76b63 --- /dev/null +++ b/app/src/main/java/com/github/mondei1/offpass/entities/Compression.kt @@ -0,0 +1,13 @@ +package com.github.mondei1.offpass.entities + +class Compression { + var key: String = "DEFAULT" + var value: String? = null + var iv: ByteArray? = null + + constructor(key: String, value: String, iv: ByteArray?) { + this.key = key + this.value = value + this.iv = iv + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/mondei1/offpass/entities/CompressionHelper.kt b/app/src/main/java/com/github/mondei1/offpass/entities/CompressionHelper.kt new file mode 100644 index 0000000..0e3577b --- /dev/null +++ b/app/src/main/java/com/github/mondei1/offpass/entities/CompressionHelper.kt @@ -0,0 +1,89 @@ +package com.github.mondei1.offpass.entities + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log +import com.github.mondei1.offpass.CryptoOperations +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.nio.charset.Charset +import kotlin.system.measureTimeMillis + +class CompressionHelper(context: Context, factory: SQLiteDatabase.CursorFactory?): + SQLiteOpenHelper(context, DATABASE_NAME, factory, DATABASE_VERSION) { + + private fun init_db(db: SQLiteDatabase) { + //db.execSQL("DROP TABLE $TABLE_NAME") + db.execSQL( + "CREATE TABLE IF NOT EXISTS `$TABLE_NAME` " + + "(`$COLUMN_KEY` VARCHAR(4) PRIMARY KEY, `$COLUMN_VALUE` TEXT, `$COLUMN_IV` BLOB," + + " `$COLUMN_SALT` BLOB)" + ) + } + + override fun onCreate(db: SQLiteDatabase) { + this.init_db(db) + } + + override fun onOpen(db: SQLiteDatabase) { + super.onOpen(db) + this.init_db(db) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + // Do nothing. We don't want to delete any sensitive values. + } + + fun add(session_key: String, compression: Compression) { + val encrypted_result = GlobalScope.async { + CryptoOperations().encrypt(session_key, compression.value!!) + } + + runBlocking { + val res = encrypted_result.await() + writableDatabase.execSQL("INSERT INTO $TABLE_NAME (`$COLUMN_KEY`, `$COLUMN_VALUE`, `$COLUMN_IV`, `$COLUMN_SALT`) " + + "VALUES (?, ?, ?, ?)", + arrayOf(compression.key, res.result, res.iv, res.salt)) + Log.d("Compression Helper", "Add key ${compression.key} = $res to database.") + } + } + + /** + * @return The encrypted content and the IV used for encryption of a specified key. + * The returned array looks like that: [key, encrypted_value, iv] + */ + suspend fun get(session_key: String, key: String): String? { + var cursor = this.readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME WHERE `key`=?", arrayOf(key)) + var list: ArrayList = ArrayList() + var salt: ByteArray = ByteArray(16) + var iv: ByteArray = ByteArray(16) + + with(cursor) { + while (moveToNext()) { + list.add(getString(getColumnIndex(COLUMN_VALUE))) + iv = getBlob(getColumnIndex(COLUMN_IV)) + salt = getBlob(getColumnIndex(COLUMN_SALT)) + } + } + + val decrypted_result = GlobalScope.async { + CryptoOperations().decrypt(list[0], session_key, iv, salt) + } + + return decrypted_result.await().toString() + } + + companion object { + private val DATABASE_VERSION = 3 + private val DATABASE_NAME = "compression.db" + val TABLE_NAME = "compression" + val COLUMN_KEY = "key" + val COLUMN_VALUE = "value" + val COLUMN_SALT = "" + val COLUMN_IV = "iv" + } +} \ No newline at end of file