package com.github.mondei1.offpass import android.content.Context import android.util.Base64 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.runBlocking import java.lang.Error class QRSchema { private var db: CompressionHelper? = null var raw: String = "" var decrypted_raw: String = "" // Parsed content lateinit var session_key: String lateinit var title: String lateinit var username: String lateinit var password: String lateinit var email: String lateinit var website_url: String var custom: HashMap = HashMap() // All defined custom/optional fields var question_awnser: HashMap = HashMap() // Used for security questions 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(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()") } // First will be to split the string into it's parts var fields: MutableList = this.decrypted_raw.split("|").toMutableList() for (i in 0 until fields.size) { // First four items have to exist and are title, username, password, URL in this order. if (i == 0) { // Check if there is a session key if (fields[0].startsWith("%")) { fields[0] = fields[0].replace("%", "") 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) } else { this.title = fields[0] } this.username = fields[1] 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("(") && 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("%") question_awnser.put(qa[0], qa[1]); } else { custom.put(key, value) } } else { throw Error("Custom/optional field should start with an open bracket: " + fields[i].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 an raw string input and will decrypt it. * * @return It returns the raw decrypted raw value. */ fun decrypt(raw: String, passphrase: String): String { val parts = raw.split(":") val iv = parts[1] val salt = parts[2] val encrypted_content = parts[3] Log.i("Decrypt schema", "Give data: $encrypted_content") this.decrypted_raw = CryptoOperations().decrypt( encrypted_content, passphrase, iv.toByteArray(), salt.toByteArray()) return this.decrypted_raw } /** * This function will take all defined variables and converts them into the QR-Code schema. * * @param compress_values Takes an string array where a entry can be `website_url` in case of an URL or * the key of an optional field. For a security question take the question as key. */ fun build(compress_values: Array, passphrase: String): String { /* First phase is to construct the encrypted data. */ val session_key = CryptoOperations().nextString(10) // used for var used_compression: Boolean = false var url_copy = website_url if (compress_values.contains("website_url")) { used_compression = true val email_key = CryptoOperations().nextString(4) this.db?.add(session_key, Compression(email_key, website_url, null, null)) url_copy = "\$$email_key" } var encrypted_content: String = "${this.title}|${this.username}|${this.password}|${this.email}|${url_copy}|" // Loop thought optional/custom fields for (i in this.custom) { if (compress_values.contains(i.key)) { used_compression = true var key_key = CryptoOperations().nextString(4) var key_value = CryptoOperations().nextString(4) this.db?.add(session_key, Compression(key_key, i.key, null, null)) this.db?.add(session_key, Compression(key_value, i.value, null, null)) encrypted_content += "(\$$key_key)\$$key_value|" } else { encrypted_content += "(${i.key})${i.value}|" } } // Loop thought security questions for (i in this.question_awnser) { used_compression = true if (compress_values.contains(i.key)) { var key_key = CryptoOperations().nextString(4) var key_value = CryptoOperations().nextString(4) this.db?.add(session_key, Compression(key_key, i.key, null, null)) this.db?.add(session_key, Compression(key_value, i.value, null, null)) encrypted_content += "()\$$key_key%\$$key_value|" } else { encrypted_content += "()${i.key}%${i.value}|" } } encrypted_content = encrypted_content.dropLast(1) if (used_compression) encrypted_content = "%$session_key%$encrypted_content" Log.i("QR-Code Builder", "Constructed encrypted content: $encrypted_content") // Now, let's encrypt that val enc = runBlocking { CryptoOperations().encrypt(passphrase, encrypted_content) } // TODO: Make schema version dynamic (currently hardcoded) var final = "op1:" + "${String(enc.iv)}:" + "${String(enc.salt)}:" + enc.result .replace("\n", "") Log.i("QR-Code Builder", "Returning final result: $final") this.raw = final return final } }