From 32c71fcb398b1ec9a3877b8c37e0fbf601f498b6 Mon Sep 17 00:00:00 2001 From: dankito Date: Wed, 7 Oct 2020 21:02:00 +0200 Subject: [PATCH] Implemented hashing user password with bcrypt --- build.gradle | 2 + .../persistence/RoomBankingPersistence.kt | 88 ++++++++---- .../persistence/BankingPersistenceJson.kt | 10 ++ ui/BankingAndroidApp/build.gradle | 1 + .../ui/android/activities/LoginActivity.kt | 4 +- .../authentication/AuthenticationService.kt | 136 ++++++------------ .../authentication/AuthenticationSettings.kt | 10 +- .../persistence/IBankingPersistence.kt | 6 +- .../persistence/NoOpBankingPersistence.kt | 9 ++ 9 files changed, 139 insertions(+), 127 deletions(-) diff --git a/build.gradle b/build.gradle index 96683fd5..bc55ebc7 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,8 @@ ext { androidXBiometricVersion = "1.0.1" + bcryptVersion = "0.9.0" + /* JavaFX */ diff --git a/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt b/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt index af565663..a1e8e3c9 100644 --- a/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt +++ b/persistence/database/RoomBankingPersistence/src/main/java/net/dankito/banking/persistence/RoomBankingPersistence.kt @@ -16,27 +16,55 @@ import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium import net.dankito.banking.util.persistence.downloadIcon import net.sqlcipher.database.SQLiteDatabase import net.sqlcipher.database.SupportFactory +import org.slf4j.LoggerFactory -open class RoomBankingPersistence(applicationContext: Context, password: String? = null) : IBankingPersistence, ITransactionPartySearcher { +open class RoomBankingPersistence(protected open val applicationContext: Context) : IBankingPersistence, ITransactionPartySearcher { companion object { + const val DatabaseName = "banking-database" + const val AppSettingsId = 1 const val FlickerCodeTanMethodSettingsId = 1 const val QrCodeTanMethodSettingsId = 2 const val PhotoTanTanMethodSettingsId = 3 + + private val log = LoggerFactory.getLogger(RoomBankingPersistence::class.java) } - protected val db: BankingDatabase - init { - val passphrase = password?.let { SQLiteDatabase.getBytes(password.toCharArray()) } ?: ByteArray(0) - val factory = SupportFactory(passphrase) + protected lateinit var database: BankingDatabase - db = Room.databaseBuilder(applicationContext, BankingDatabase::class.java, "banking-database") - .openHelperFactory(factory) - .build() + + override fun decryptData(password: String?): Boolean { + return openDatabase(password) + } + + override fun changePassword(newPassword: String?) { + if (this::database.isInitialized) { + database.query("PRAGMA rekey = '$newPassword';", emptyArray()) + } + else { // database hasn't been opened yet, that means we're on the first app run + openDatabase(newPassword) + } + } + + protected open fun openDatabase(password: String?): Boolean { + try { + val passphrase = password?.let { SQLiteDatabase.getBytes(password.toCharArray()) } ?: ByteArray(0) + val factory = SupportFactory(passphrase) + + database = Room.databaseBuilder(applicationContext, BankingDatabase::class.java, DatabaseName) + .openHelperFactory(factory) + .build() + + return true + } catch (e: Exception) { + log.error("Could not open database", e) + } + + return false } @@ -44,22 +72,22 @@ open class RoomBankingPersistence(applicationContext: Context, password: String? (bank as? Bank)?.let { bank -> bank.selectedTanMethodId = bank.selectedTanMethod?.technicalId - db.bankDao().saveOrUpdate(bank) + database.bankDao().saveOrUpdate(bank) // TODO: in this way removed accounts won't be deleted from DB and therefore still be visible to user val accounts = bank.accounts.filterIsInstance() accounts.forEach { it.bankId = bank.id } - db.bankAccountDao().saveOrUpdate(accounts) + database.bankAccountDao().saveOrUpdate(accounts) // TODO: in this way removed TAN methods won't be deleted from DB and therefore still be visible to user val tanMethods = bank.supportedTanMethods.filterIsInstance() tanMethods.forEach { tantanMethod -> if (tantanMethod.bankId == BaseDao.ObjectNotInsertedId) { tantanMethod.bankId = bank.id - db.tanMethodDao().insert(tantanMethod) + database.tanMethodDao().insert(tantanMethod) } else { - db.tanMethodDao().update(tantanMethod) + database.tanMethodDao().update(tantanMethod) } } @@ -67,34 +95,34 @@ open class RoomBankingPersistence(applicationContext: Context, password: String? val tanMedia = bank.tanMedia.map { tanMedium -> bank.tanMediumEntities.firstOrNull { it.id == tanMedium.technicalId } ?: map(bank, tanMedium) } - db.tanMediumDao().saveOrUpdate(tanMedia) + database.tanMediumDao().saveOrUpdate(tanMedia) bank.tanMediumEntities = tanMedia } } override fun deleteBank(bank: TypedBankData, allBanks: List) { (bank as? Bank)?.let { bank -> - db.accountTransactionDao().delete(bank.accounts.flatMap { it.bookedTransactions }.filterIsInstance()) + database.accountTransactionDao().delete(bank.accounts.flatMap { it.bookedTransactions }.filterIsInstance()) - db.bankAccountDao().delete(bank.accounts.filterIsInstance()) + database.bankAccountDao().delete(bank.accounts.filterIsInstance()) - db.tanMethodDao().delete(bank.supportedTanMethods.filterIsInstance()) - db.tanMediumDao().delete(bank.tanMedia.filterIsInstance()) + database.tanMethodDao().delete(bank.supportedTanMethods.filterIsInstance()) + database.tanMediumDao().delete(bank.tanMedia.filterIsInstance()) - db.bankDao().delete(bank) + database.bankDao().delete(bank) } } override fun readPersistedBanks(): List { - val banks = db.bankDao().getAll() + val banks = database.bankDao().getAll() - val accounts = db.bankAccountDao().getAll() + val accounts = database.bankAccountDao().getAll() - val transactions = db.accountTransactionDao().getAll() + val transactions = database.accountTransactionDao().getAll() - val tanMethods = db.tanMethodDao().getAll() + val tanMethods = database.tanMethodDao().getAll() - val tanMedia = db.tanMediumDao().getAll() + val tanMedia = database.tanMediumDao().getAll() banks.forEach { bank -> bank.accounts = accounts.filter { it.bankId == bank.id } @@ -126,7 +154,7 @@ open class RoomBankingPersistence(applicationContext: Context, password: String? mappedTransactions.forEach { it.accountId = accountId } - db.accountTransactionDao().saveOrUpdate(mappedTransactions) + database.accountTransactionDao().saveOrUpdate(mappedTransactions) } @@ -159,7 +187,7 @@ open class RoomBankingPersistence(applicationContext: Context, password: String? override fun saveOrUpdateAppSettings(appSettings: AppSettings) { val mapped = net.dankito.banking.persistence.model.AppSettings(appSettings.updateAccountsAutomatically, appSettings.refreshAccountsAfterMinutes) - db.appSettingsDao().saveOrUpdate(mapped) + database.appSettingsDao().saveOrUpdate(mapped) saveOrUpdateTanMethodSettings(appSettings.flickerCodeSettings, FlickerCodeTanMethodSettingsId) saveOrUpdateTanMethodSettings(appSettings.qrCodeSettings, QrCodeTanMethodSettingsId) @@ -170,16 +198,16 @@ open class RoomBankingPersistence(applicationContext: Context, password: String? settings?.let { val settingsEntity = TanMethodSettings(id, it.width, it.height, it.space, it.frequency) - db.tanMethodSettingsDao().saveOrUpdate(settingsEntity) + database.tanMethodSettingsDao().saveOrUpdate(settingsEntity) } } override fun readPersistedAppSettings(): AppSettings? { - val tanMethodSettings = db.tanMethodSettingsDao().getAll() + val tanMethodSettings = database.tanMethodSettingsDao().getAll() val settings = AppSettings() - db.appSettingsDao().getAll().firstOrNull { it.id == AppSettingsId }?.let { persistedSettings -> + database.appSettingsDao().getAll().firstOrNull { it.id == AppSettingsId }?.let { persistedSettings -> settings.updateAccountsAutomatically = persistedSettings.updateAccountsAutomatically settings.refreshAccountsAfterMinutes = persistedSettings.refreshAccountsAfterMinutes } @@ -201,13 +229,13 @@ open class RoomBankingPersistence(applicationContext: Context, password: String? bank.iconData = iconData (bank as? Bank)?.let { - db.bankDao().saveOrUpdate(it) + database.bankDao().saveOrUpdate(it) } } override fun findTransactionParty(query: String): List { - return db.accountTransactionDao().findTransactionParty(query) + return database.accountTransactionDao().findTransactionParty(query) .toSet() // don't display same transaction party multiple times .filterNot { it.bankCode.isNullOrBlank() || it.accountId.isNullOrBlank() } .map { TransactionParty(it.name, it.accountId, it.bankCode) } diff --git a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt index c7b8d8b3..22947d19 100644 --- a/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt +++ b/persistence/json/BankingPersistenceJson/src/main/kotlin/net/dankito/banking/persistence/BankingPersistenceJson.kt @@ -35,6 +35,16 @@ open class BankingPersistenceJson( } + override fun decryptData(password: String?): Boolean { + // TODO: may implement data decryption. But then we have to store password to be able to encrypt data + return true + } + + override fun changePassword(newPassword: String?) { + // TODO: may implement data decryption. But then we have to store newPassword to be able to encrypt data + } + + override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List) { saveAllBanks(allBanks) } diff --git a/ui/BankingAndroidApp/build.gradle b/ui/BankingAndroidApp/build.gradle index 7a90c36c..88cd4011 100644 --- a/ui/BankingAndroidApp/build.gradle +++ b/ui/BankingAndroidApp/build.gradle @@ -131,6 +131,7 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" implementation "androidx.biometric:biometric:$androidXBiometricVersion" + implementation "at.favre.lib:bcrypt:$bcryptVersion" implementation "com.mikepenz:fastadapter:$fastAdapterVersion" implementation "com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion" diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt index 2e9aa57f..b0f91ce6 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt @@ -59,8 +59,7 @@ open class LoginActivity : BaseActivity() { val enteredPassword = edtxtLoginPassword.text - if (authenticationService.isCorrectUserPassword(enteredPassword)) { - authenticationService.userLoggedInWithPassword(enteredPassword) + if (authenticationService.authenticateUserWithPassword(enteredPassword)) { navigateToMainActivity() } else { @@ -71,7 +70,6 @@ open class LoginActivity : BaseActivity() { } protected open fun biometricAuthenticationSuccessful() { - authenticationService.userLoggedInWithBiometricAuthentication() navigateToMainActivity() } diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt index 56c7ef7e..58ec5d06 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt @@ -1,5 +1,6 @@ package net.dankito.banking.ui.android.authentication +import at.favre.lib.crypto.bcrypt.BCrypt import net.dankito.banking.persistence.IBankingPersistence import net.dankito.banking.util.ISerializer import net.dankito.utils.multiplatform.File @@ -14,8 +15,6 @@ open class AuthenticationService( ) { companion object { - private const val AuthenticationTypeFilename = "a" - private const val AuthenticationSettingsFilename = "s" private val log = LoggerFactory.getLogger(AuthenticationService::class.java) @@ -30,130 +29,85 @@ open class AuthenticationService( init { - authenticationType = loadAuthenticationType() + val settings = loadAuthenticationSettings() - if (authenticationType == AuthenticationType.None) { - val authenticationSettings = loadAuthenticationSettings() + if (settings == null) { // first app run -> create a default password + removeAppProtection() + } + else { + authenticationType = settings.type - if (authenticationSettings == null) { // first app run -> create a default password - removeAppProtection() - } - else { - openDatabase(authenticationSettings) + if (settings.type == AuthenticationType.None) { + openDatabase(settings) } } } - open fun userLoggedInWithBiometricAuthentication() { - loadAuthenticationSettings()?.let { - openDatabase(it) + open fun authenticateUserWithPassword(enteredPassword: String): Boolean { + if (isCorrectUserPassword(enteredPassword)) { + return openDatabase(enteredPassword) } + + return false } - open fun userLoggedInWithPassword(enteredPassword: String) { - openDatabase(enteredPassword) + open fun isCorrectUserPassword(enteredPassword: String): Boolean { + loadAuthenticationSettings()?.let { settings -> + val result = BCrypt.verifyer().verify(enteredPassword.toCharArray(), settings.hashedUserPassword) + + return result.verified + } + + return false } protected open fun openDatabase(authenticationSettings: AuthenticationSettings) { openDatabase(authenticationSettings.userPassword) } - protected open fun openDatabase(password: String?) { - persistence.decryptData(password) + protected open fun openDatabase(password: String?): Boolean { + return persistence.decryptData(password) } + open fun setAuthenticationMethodToBiometric() { - if (saveNewUserPassword(generateRandomPassword())) { - if (saveAuthenticationType(AuthenticationType.Biometric)) { - authenticationType = AuthenticationType.Biometric - } - } + saveNewAuthenticationMethod(AuthenticationType.Biometric, generateRandomPassword()) } open fun setAuthenticationMethodToPassword(newPassword: String) { - if (saveNewUserPassword(newPassword)) { - if (saveAuthenticationType(AuthenticationType.Password)) { - authenticationType = AuthenticationType.Password - } - } + saveNewAuthenticationMethod(AuthenticationType.Password, newPassword) } open fun removeAppProtection() { - if (saveNewUserPassword(generateRandomPassword())) { - if (saveAuthenticationType(AuthenticationType.None)) { - authenticationType = AuthenticationType.None - } - } + saveNewAuthenticationMethod(AuthenticationType.None, generateRandomPassword()) } - open fun isCorrectUserPassword(password: String): Boolean { - loadAuthenticationSettings()?.let { settings -> - return settings.userPassword == password - } - - return false - } - - - protected open fun loadAuthenticationType(): AuthenticationType { - try { - val file = File(dataFolder, AuthenticationTypeFilename) - - if (file.exists()) { - - val fileContent = file.readText() - - return when (fileContent.toInt()) { - AuthenticationType.Password.rawValue -> AuthenticationType.Password - AuthenticationType.Biometric.rawValue -> AuthenticationType.Biometric - AuthenticationType.None.rawValue -> AuthenticationType.None - else -> AuthenticationType.None - } - } - } catch (e: Exception) { - log.error("Could not load AuthenticationType", e) - } - - return AuthenticationType.None - } - - protected open fun saveAuthenticationType(type: AuthenticationType): Boolean { - try { - val file = File(dataFolder, AuthenticationTypeFilename) - - file.writeText(type.rawValue.toString()) - - return true - } catch (e: Exception) { - log.error("Could not save AuthenticationType", e) - } - - return false - } - - - protected open fun saveNewUserPassword(newPassword: String?): Boolean { + protected open fun saveNewAuthenticationMethod(type: AuthenticationType, newPassword: String): Boolean { val settings = loadOrCreateDefaultAuthenticationSettings() - val currentPassword = settings.userPassword - if (currentPassword != newPassword) { - settings.userPassword = newPassword - - if (saveAuthenticationSettings(settings)) { - persistence.changePassword(currentPassword, newPassword) // TODO: actually this is bad. If changing password fails then password is saved in AuthenticationSettings but DB has a different password - return true - } - - return false + if (type == settings.type && + ((type != AuthenticationType.Password && settings.userPassword == newPassword) + || (type == AuthenticationType.Password && isCorrectUserPassword(newPassword)))) { // nothing changed + return true } - return true + settings.type = type + settings.hashedUserPassword = if (type == AuthenticationType.Password) BCrypt.withDefaults().hashToString(12, newPassword.toCharArray()) else null + settings.userPassword = if (type == AuthenticationType.Password) null else newPassword + + if (saveAuthenticationSettings(settings)) { + this.authenticationType = type + persistence.changePassword(newPassword) // TODO: actually this is bad. If changing password fails then password is saved in AuthenticationSettings but DB has a different password + return true + } + + return false } protected open fun loadOrCreateDefaultAuthenticationSettings(): AuthenticationSettings { - return loadAuthenticationSettings() ?: AuthenticationSettings(null) + return loadAuthenticationSettings() ?: AuthenticationSettings(AuthenticationType.None) } protected open fun loadAuthenticationSettings(): AuthenticationSettings? { diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt index a3feb220..aa29ec06 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt @@ -2,5 +2,11 @@ package net.dankito.banking.ui.android.authentication open class AuthenticationSettings( - var userPassword: String? = null -) \ No newline at end of file + open var type: AuthenticationType, + open var hashedUserPassword: String? = null, + open var userPassword: String? = null +) { + + internal constructor() : this(AuthenticationType.None) // for object deserializers + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt index d2998789..778c67bb 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/IBankingPersistence.kt @@ -2,11 +2,15 @@ package net.dankito.banking.persistence import net.dankito.banking.ui.model.* import net.dankito.banking.ui.model.settings.AppSettings -import net.dankito.utils.multiplatform.File interface IBankingPersistence { + fun decryptData(password: String?): Boolean + + fun changePassword(newPassword: String?) + + fun saveOrUpdateBank(bank: TypedBankData, allBanks: List) fun deleteBank(bank: TypedBankData, allBanks: List) diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt index db94a4bb..9b2718cc 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/persistence/NoOpBankingPersistence.kt @@ -6,6 +6,15 @@ import net.dankito.banking.ui.model.settings.AppSettings open class NoOpBankingPersistence : IBankingPersistence { + override fun decryptData(password: String?): Boolean { + return true + } + + override fun changePassword(newPassword: String?) { + + } + + override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List) { }