Files
offpass-android/app/src/main/java/com/github/mondei1/offpass/QRSchema.kt
2020-07-05 18:16:37 +02:00

231 lines
8.4 KiB
Kotlin

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