change aes, now more secure, beacuse no hardcode anymore

This commit is contained in:
oxidiert
2025-09-02 13:01:41 +02:00
parent 073f33a9bb
commit 0992304e59
4 changed files with 117 additions and 221 deletions

View File

@ -1,56 +1,73 @@
// app/src/main/java/com/dano/test1/AES256Helper.kt
package com.dano.test1
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
import kotlin.math.min
object AES256Helper {
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
private const val ALGORITHM = "AES"
private const val IV_SIZE = 16
// HKDF-SHA256: IKM = tokenHex->bytes, salt="", info="qdb-aes", len=32
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
val ikm = hexToBytes(tokenHex)
val mac = Mac.getInstance("HmacSHA256")
val zeroSalt = ByteArray(32) { 0 }
mac.init(SecretKeySpec(zeroSalt, "HmacSHA256"))
val prk = mac.doFinal(ikm)
// Beispiel-Key: 32 Bytes = 256 bit. Ersetze das durch deinen eigenen sicheren Schlüssel!
private val keyBytes = "12345678901234567890123456789012".toByteArray(Charsets.UTF_8)
private val secretKey = SecretKeySpec(keyBytes, ALGORITHM)
// Verschlüsseln: InputFile -> OutputFile (mit zufälligem IV vorne in der Datei)
fun encryptFile(inputFile: File, outputFile: File) {
val iv = ByteArray(IV_SIZE)
Random.nextBytes(iv)
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
FileOutputStream(outputFile).use { fileOut ->
// IV vorne reinschreiben
fileOut.write(iv)
CipherOutputStream(fileOut, cipher).use { cipherOut ->
FileInputStream(inputFile).use { fileIn ->
fileIn.copyTo(cipherOut)
}
}
var previous = ByteArray(0)
val okm = ByteArray(len)
var generated = 0
var counter = 1
while (generated < len) {
mac.init(SecretKeySpec(prk, "HmacSHA256"))
mac.update(previous)
mac.update(info.toByteArray(Charsets.UTF_8))
mac.update(counter.toByte())
val t = mac.doFinal()
val toCopy = min(len - generated, t.size)
System.arraycopy(t, 0, okm, generated, toCopy)
previous = t
generated += toCopy
counter++
}
return okm
}
// Entschlüsseln: InputFile (IV+Ciphertext) -> OutputFile (Klartext)
fun decryptFile(inputFile: File, outputFile: File) {
FileInputStream(inputFile).use { fileIn ->
val iv = ByteArray(IV_SIZE)
if (fileIn.read(iv) != IV_SIZE) throw IllegalArgumentException("Ungültige Datei oder IV fehlt")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
CipherInputStream(fileIn, cipher).use { cipherIn ->
FileOutputStream(outputFile).use { fileOut ->
cipherIn.copyTo(fileOut)
}
}
private fun hexToBytes(hex: String): ByteArray {
val clean = hex.trim()
val len = clean.length
val out = ByteArray(len / 2)
var i = 0
while (i < len) {
out[i / 2] = ((Character.digit(clean[i], 16) shl 4) + Character.digit(clean[i + 1], 16)).toByte()
i += 2
}
return out
}
fun encryptFileWithToken(inFile: File, outFile: File, token: String) {
val key = hkdfFromToken(token)
val iv = ByteArray(16).also { SecureRandom().nextBytes(it) }
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
val plain = inFile.readBytes()
val enc = cipher.doFinal(plain)
outFile.writeBytes(iv + enc)
}
fun decryptFileWithToken(inFile: File, token: String): ByteArray {
val key = hkdfFromToken(token)
val data = inFile.readBytes()
require(data.size >= 16) { "cipher too short" }
val iv = data.copyOfRange(0, 16)
val ct = data.copyOfRange(16, data.size)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
return cipher.doFinal(ct)
}
}

View File

@ -1,3 +1,4 @@
// app/src/main/java/com/dano/test1/DatabaseDownloader.kt
package com.dano.test1
import android.content.Context
@ -9,28 +10,17 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object DatabaseDownloader {
private const val DB_NAME = "questionnaire_database"
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php"
// AES-256 Key (muss exakt 32 Bytes lang sein)
private const val AES_KEY = "12345678901234567890123456789012"
private val client = OkHttpClient()
/**
* Startet den Download und Austausch der DB, benötigt gültiges Token
*/
fun downloadAndReplaceDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL")
val request = Request.Builder()
.url(SERVER_DOWNLOAD_URL)
.header("Authorization", "Bearer $token")
@ -38,47 +28,24 @@ object DatabaseDownloader {
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
Log.e("DOWNLOAD", "Fehler beim Download: ${response.code}")
Log.e("DOWNLOAD", "HTTP ${response.code}")
return@launch
}
// Zwischenspeichern der verschlüsselten Datei
val downloadedFile = File(context.cacheDir, "downloaded_database.enc")
val encFile = File(context.cacheDir, "downloaded_database.enc")
response.body?.byteStream()?.use { input ->
FileOutputStream(downloadedFile).use { output ->
input.copyTo(output)
}
FileOutputStream(encFile).use { output -> input.copyTo(output) }
}
Log.d("DOWNLOAD", "Datei gespeichert: ${downloadedFile.absolutePath}")
// Entschlüsselung
val decryptedBytes = decryptFile(downloadedFile)
val decryptedBytes = AES256Helper.decryptFileWithToken(encFile, token)
val dbFile = context.getDatabasePath(DB_NAME)
if (dbFile.exists()) dbFile.delete()
FileOutputStream(dbFile).use { fos ->
fos.write(decryptedBytes)
}
Log.d("DOWNLOAD", "Neue DB erfolgreich entschlüsselt und eingesetzt")
FileOutputStream(dbFile).use { it.write(decryptedBytes) }
Log.d("DOWNLOAD", "DB erfolgreich ersetzt")
} catch (e: Exception) {
Log.e("DOWNLOAD", "Fehler beim Download oder Ersetzen der DB", e)
Log.e("DOWNLOAD", "Fehler", e)
}
}
}
private fun decryptFile(file: File): ByteArray {
val fileBytes = file.readBytes()
if (fileBytes.size < 16) throw IllegalArgumentException("Datei zu kurz, kein IV vorhanden")
val iv = fileBytes.copyOfRange(0, 16)
val cipherBytes = fileBytes.copyOfRange(16, fileBytes.size)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val keySpec = SecretKeySpec(AES_KEY.toByteArray(Charsets.UTF_8), "AES")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
return cipher.doFinal(cipherBytes)
}
}

View File

@ -1,3 +1,4 @@
// app/src/main/java/com/dano/test1/DatabaseUploader.kt
package com.dano.test1
import android.content.Context
@ -25,181 +26,111 @@ object DatabaseUploader {
private val client = OkHttpClient()
/**
* Startet den Upload mit Login über LoginManager.
* @param context Android Context
* @param password Vom User eingegebenes Passwort
*/
fun uploadDatabaseWithLogin(context: Context, password: String) {
LoginManager.loginUser(context, password,
onSuccess = { token ->
Log.d("UPLOAD", "Login erfolgreich, Token erhalten")
Log.d("UPLOAD", "Login OK")
uploadDatabase(context, token)
},
onError = { errorMsg ->
Log.e("UPLOAD", "Login fehlgeschlagen: $errorMsg")
}
onError = { msg -> Log.e("UPLOAD", "Login fehlgeschlagen: $msg") }
)
}
/**
* Interner Upload, benötigt gültiges Token
*/
private fun uploadDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val dbFile = context.getDatabasePath(DB_NAME)
if (!dbFile.exists()) {
Log.e("UPLOAD", "Datenbankdatei existiert nicht: ${dbFile.absolutePath}")
Log.e("UPLOAD", "DB fehlt: ${dbFile.absolutePath}")
return@launch
}
// WAL-Checkpoint
try {
val db = SQLiteDatabase.openDatabase(
dbFile.absolutePath,
null,
SQLiteDatabase.OPEN_READWRITE
)
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { cursor ->
if (cursor.moveToFirst()) {
try { Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}") } catch (_: Exception) {}
}
}
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* ignore */ }
db.close()
Log.d("UPLOAD", "WAL-Checkpoint erfolgreich.")
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim WAL-Checkpoint", e)
}
val exists = checkDatabaseExists()
if (exists) {
Log.d("UPLOAD", "Server-Datenbank vorhanden → Delta-Upload")
} else {
Log.d("UPLOAD", "Keine Server-Datenbank → Delta-Upload")
}
} catch (_: Exception) { }
checkDatabaseExists() // nur Logging
uploadPseudoDelta(context, dbFile, token)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim Hochladen der DB", e)
Log.e("UPLOAD", "Fehler", e)
}
}
}
private fun checkDatabaseExists(): Boolean {
return try {
val request = Request.Builder()
.url(SERVER_CHECK_URL)
.get()
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
Log.e("UPLOAD", "checkDatabaseExists HTTP error: ${response.code}")
return false
}
val body = response.body?.string() ?: return false
try {
val j = JSONObject(body)
j.optBoolean("exists", false)
} catch (e: Exception) {
body.contains("exists", ignoreCase = true)
}
val req = Request.Builder().url(SERVER_CHECK_URL).get().build()
client.newCall(req).execute().use { resp ->
if (!resp.isSuccessful) return false
val body = resp.body?.string() ?: return false
try { JSONObject(body).optBoolean("exists", false) } catch (_: Exception) { false }
}
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler bei Server-Prüfung", e)
false
}
} catch (e: Exception) { false }
}
private fun uploadPseudoDelta(context: Context, file: File, token: String) {
try {
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
val data = JSONObject().apply {
put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients"))
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
put(
"completed_questionnaires",
queryToJsonArray(
db,
"SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"
)
)
put("completed_questionnaires",
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"))
}
db.close()
val tmpJson = File(context.cacheDir, "payload.json")
tmpJson.writeText(data.toString())
val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
val tmpEnc = File(context.cacheDir, "payload.enc")
try {
AES256Helper.encryptFile(tmpJson, tmpEnc)
AES256Helper.encryptFileWithToken(tmpJson, tmpEnc, token)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e)
tmpJson.delete()
return
Log.e("UPLOAD", "Verschlüsselung fehlgeschlagen", e)
tmpJson.delete(); return
}
val requestBody = MultipartBody.Builder()
val body = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("token", token) // Token vom Login
.addFormDataPart(
"file",
"payload.enc",
tmpEnc.asRequestBody("application/octet-stream".toMediaType())
)
.build()
val request = Request.Builder()
.url(SERVER_DELTA_URL)
.post(requestBody)
.addFormDataPart("token", token)
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
.build()
val request = Request.Builder().url(SERVER_DELTA_URL).post(body).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}")
tmpJson.delete()
tmpEnc.delete()
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
tmpJson.delete(); tmpEnc.delete()
}
override fun onResponse(call: Call, response: Response) {
val body = try { response.body?.string() ?: "Keine Response" } catch (e: Exception) {
"Fehler beim Lesen der Response: ${e.message}"
}
val respBody = try { response.body?.string() ?: "" } catch (_: Exception) { "" }
if (response.isSuccessful) {
Log.d("UPLOAD", "Delta-Upload erfolgreich: $body")
if (file.delete()) Log.d("UPLOAD", "Lokale DB gelöscht.") else Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
val journalFile = File(file.parent, file.name + "-journal")
if (journalFile.exists() && journalFile.delete()) Log.d("UPLOAD", "Journal-Datei gelöscht.")
tmpJson.delete()
tmpEnc.delete()
exitProcess(0)
Log.d("UPLOAD", "OK: $respBody")
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
File(file.parent, file.name + "-journal").delete()
} else {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${response.code} $body")
tmpJson.delete()
tmpEnc.delete()
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
}
tmpJson.delete(); tmpEnc.delete()
try { exitProcess(0) } catch (_: Exception) {}
}
})
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim Delta-Upload", e)
Log.e("UPLOAD", "Exception", e)
}
}
private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray {
val cursor = db.rawQuery(query, null)
val jsonArray = JSONArray()
cursor.use {
val columnNames = it.columnNames
val c = db.rawQuery(query, null)
val arr = JSONArray()
c.use {
val cols = it.columnNames
while (it.moveToNext()) {
val obj = JSONObject()
for (col in columnNames) {
for (col in cols) {
val idx = it.getColumnIndex(col)
if (idx >= 0) {
when (it.getType(idx)) {
@ -207,17 +138,14 @@ object DatabaseUploader {
Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx))
Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx))
Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL)
Cursor.FIELD_TYPE_BLOB -> {
val blob = it.getBlob(idx)
obj.put(col, Base64.encodeToString(blob, Base64.NO_WRAP))
}
Cursor.FIELD_TYPE_BLOB -> obj.put(col, Base64.encodeToString(it.getBlob(idx), Base64.NO_WRAP))
else -> obj.put(col, it.getString(idx))
}
}
}
jsonArray.put(obj)
arr.put(obj)
}
}
return jsonArray
return arr
}
}

View File

@ -1,3 +1,4 @@
// app/src/main/java/com/dano/test1/LoginManager.kt
package com.dano.test1
import android.content.Context
@ -13,18 +14,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
object LoginManager {
private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
private val client = OkHttpClient()
/**
* Startet den Login-Prozess.
*
* @param context Android Context
* @param password Vom User eingegebenes Passwort
* @param onSuccess Callback mit dem Token wenn Login erfolgreich
* @param onError Callback mit Fehlermeldung
*/
fun loginUser(
context: Context,
password: String,
@ -46,26 +38,18 @@ object LoginManager {
if (response.isSuccessful && responseText != null) {
val json = JSONObject(responseText)
if (json.getBoolean("success")) {
if (json.optBoolean("success")) {
val token = json.getString("token")
withContext(Dispatchers.Main) {
onSuccess(token)
}
withContext(Dispatchers.Main) { onSuccess(token) }
} else {
withContext(Dispatchers.Main) {
onError("Login fehlgeschlagen")
}
withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") }
}
} else {
withContext(Dispatchers.Main) {
onError("Fehler beim Login (${response.code})")
}
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
}
} catch (e: Exception) {
Log.e("LOGIN", "Exception beim Login", e)
withContext(Dispatchers.Main) {
onError("Exception: ${e.message}")
}
Log.e("LOGIN", "Exception", e)
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
}
}
}