231 lines
8.4 KiB
Kotlin
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
|
|
|
|
}
|
|
|
|
} |