Add compression support

This commit is contained in:
2020-07-03 16:28:26 +02:00
parent a8e6c9e99b
commit b1b4ad3030
7 changed files with 342 additions and 13 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String, String> = HashMap() // All defined custom/optional fields
var question_awnser: HashMap<String, String> = 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<String> {
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() {
}

View File

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

View File

@@ -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<String> = ArrayList<String>()
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"
}
}