Add compression support
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
136
app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt
Normal file
136
app/src/main/java/com/github/mondei1/offpass/CryptoOperations.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user