Implemented hashing user password with bcrypt

This commit is contained in:
dankito 2020-10-07 21:02:00 +02:00
parent d65b766655
commit 32c71fcb39
9 changed files with 139 additions and 127 deletions

View File

@ -73,6 +73,8 @@ ext {
androidXBiometricVersion = "1.0.1" androidXBiometricVersion = "1.0.1"
bcryptVersion = "0.9.0"
/* JavaFX */ /* JavaFX */

View File

@ -16,27 +16,55 @@ import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
import net.dankito.banking.util.persistence.downloadIcon import net.dankito.banking.util.persistence.downloadIcon
import net.sqlcipher.database.SQLiteDatabase import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactory 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 { companion object {
const val DatabaseName = "banking-database"
const val AppSettingsId = 1 const val AppSettingsId = 1
const val FlickerCodeTanMethodSettingsId = 1 const val FlickerCodeTanMethodSettingsId = 1
const val QrCodeTanMethodSettingsId = 2 const val QrCodeTanMethodSettingsId = 2
const val PhotoTanTanMethodSettingsId = 3 const val PhotoTanTanMethodSettingsId = 3
private val log = LoggerFactory.getLogger(RoomBankingPersistence::class.java)
} }
protected val db: BankingDatabase
init { protected lateinit var database: BankingDatabase
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 passphrase = password?.let { SQLiteDatabase.getBytes(password.toCharArray()) } ?: ByteArray(0)
val factory = SupportFactory(passphrase) val factory = SupportFactory(passphrase)
db = Room.databaseBuilder(applicationContext, BankingDatabase::class.java, "banking-database") database = Room.databaseBuilder(applicationContext, BankingDatabase::class.java, DatabaseName)
.openHelperFactory(factory) .openHelperFactory(factory)
.build() .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 as? Bank)?.let { bank ->
bank.selectedTanMethodId = bank.selectedTanMethod?.technicalId 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 // TODO: in this way removed accounts won't be deleted from DB and therefore still be visible to user
val accounts = bank.accounts.filterIsInstance<BankAccount>() val accounts = bank.accounts.filterIsInstance<BankAccount>()
accounts.forEach { it.bankId = bank.id } 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 // 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<TanMethod>() val tanMethods = bank.supportedTanMethods.filterIsInstance<TanMethod>()
tanMethods.forEach { tantanMethod -> tanMethods.forEach { tantanMethod ->
if (tantanMethod.bankId == BaseDao.ObjectNotInsertedId) { if (tantanMethod.bankId == BaseDao.ObjectNotInsertedId) {
tantanMethod.bankId = bank.id tantanMethod.bankId = bank.id
db.tanMethodDao().insert(tantanMethod) database.tanMethodDao().insert(tantanMethod)
} }
else { 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 -> val tanMedia = bank.tanMedia.map { tanMedium ->
bank.tanMediumEntities.firstOrNull { it.id == tanMedium.technicalId } ?: map(bank, tanMedium) bank.tanMediumEntities.firstOrNull { it.id == tanMedium.technicalId } ?: map(bank, tanMedium)
} }
db.tanMediumDao().saveOrUpdate(tanMedia) database.tanMediumDao().saveOrUpdate(tanMedia)
bank.tanMediumEntities = tanMedia bank.tanMediumEntities = tanMedia
} }
} }
override fun deleteBank(bank: TypedBankData, allBanks: List<TypedBankData>) { override fun deleteBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
(bank as? Bank)?.let { bank -> (bank as? Bank)?.let { bank ->
db.accountTransactionDao().delete(bank.accounts.flatMap { it.bookedTransactions }.filterIsInstance<AccountTransaction>()) database.accountTransactionDao().delete(bank.accounts.flatMap { it.bookedTransactions }.filterIsInstance<AccountTransaction>())
db.bankAccountDao().delete(bank.accounts.filterIsInstance<BankAccount>()) database.bankAccountDao().delete(bank.accounts.filterIsInstance<BankAccount>())
db.tanMethodDao().delete(bank.supportedTanMethods.filterIsInstance<TanMethod>()) database.tanMethodDao().delete(bank.supportedTanMethods.filterIsInstance<TanMethod>())
db.tanMediumDao().delete(bank.tanMedia.filterIsInstance<TanMedium>()) database.tanMediumDao().delete(bank.tanMedia.filterIsInstance<TanMedium>())
db.bankDao().delete(bank) database.bankDao().delete(bank)
} }
} }
override fun readPersistedBanks(): List<TypedBankData> { override fun readPersistedBanks(): List<TypedBankData> {
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 -> banks.forEach { bank ->
bank.accounts = accounts.filter { it.bankId == bank.id } bank.accounts = accounts.filter { it.bankId == bank.id }
@ -126,7 +154,7 @@ open class RoomBankingPersistence(applicationContext: Context, password: String?
mappedTransactions.forEach { it.accountId = accountId } 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) { override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
val mapped = net.dankito.banking.persistence.model.AppSettings(appSettings.updateAccountsAutomatically, appSettings.refreshAccountsAfterMinutes) 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.flickerCodeSettings, FlickerCodeTanMethodSettingsId)
saveOrUpdateTanMethodSettings(appSettings.qrCodeSettings, QrCodeTanMethodSettingsId) saveOrUpdateTanMethodSettings(appSettings.qrCodeSettings, QrCodeTanMethodSettingsId)
@ -170,16 +198,16 @@ open class RoomBankingPersistence(applicationContext: Context, password: String?
settings?.let { settings?.let {
val settingsEntity = TanMethodSettings(id, it.width, it.height, it.space, it.frequency) val settingsEntity = TanMethodSettings(id, it.width, it.height, it.space, it.frequency)
db.tanMethodSettingsDao().saveOrUpdate(settingsEntity) database.tanMethodSettingsDao().saveOrUpdate(settingsEntity)
} }
} }
override fun readPersistedAppSettings(): AppSettings? { override fun readPersistedAppSettings(): AppSettings? {
val tanMethodSettings = db.tanMethodSettingsDao().getAll() val tanMethodSettings = database.tanMethodSettingsDao().getAll()
val settings = AppSettings() 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.updateAccountsAutomatically = persistedSettings.updateAccountsAutomatically
settings.refreshAccountsAfterMinutes = persistedSettings.refreshAccountsAfterMinutes settings.refreshAccountsAfterMinutes = persistedSettings.refreshAccountsAfterMinutes
} }
@ -201,13 +229,13 @@ open class RoomBankingPersistence(applicationContext: Context, password: String?
bank.iconData = iconData bank.iconData = iconData
(bank as? Bank)?.let { (bank as? Bank)?.let {
db.bankDao().saveOrUpdate(it) database.bankDao().saveOrUpdate(it)
} }
} }
override fun findTransactionParty(query: String): List<TransactionParty> { override fun findTransactionParty(query: String): List<TransactionParty> {
return db.accountTransactionDao().findTransactionParty(query) return database.accountTransactionDao().findTransactionParty(query)
.toSet() // don't display same transaction party multiple times .toSet() // don't display same transaction party multiple times
.filterNot { it.bankCode.isNullOrBlank() || it.accountId.isNullOrBlank() } .filterNot { it.bankCode.isNullOrBlank() || it.accountId.isNullOrBlank() }
.map { TransactionParty(it.name, it.accountId, it.bankCode) } .map { TransactionParty(it.name, it.accountId, it.bankCode) }

View File

@ -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<TypedBankData>) { override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
saveAllBanks(allBanks) saveAllBanks(allBanks)
} }

View File

@ -131,6 +131,7 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation "androidx.biometric:biometric:$androidXBiometricVersion" implementation "androidx.biometric:biometric:$androidXBiometricVersion"
implementation "at.favre.lib:bcrypt:$bcryptVersion"
implementation "com.mikepenz:fastadapter:$fastAdapterVersion" implementation "com.mikepenz:fastadapter:$fastAdapterVersion"
implementation "com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion" implementation "com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion"

View File

@ -59,8 +59,7 @@ open class LoginActivity : BaseActivity() {
val enteredPassword = edtxtLoginPassword.text val enteredPassword = edtxtLoginPassword.text
if (authenticationService.isCorrectUserPassword(enteredPassword)) { if (authenticationService.authenticateUserWithPassword(enteredPassword)) {
authenticationService.userLoggedInWithPassword(enteredPassword)
navigateToMainActivity() navigateToMainActivity()
} }
else { else {
@ -71,7 +70,6 @@ open class LoginActivity : BaseActivity() {
} }
protected open fun biometricAuthenticationSuccessful() { protected open fun biometricAuthenticationSuccessful() {
authenticationService.userLoggedInWithBiometricAuthentication()
navigateToMainActivity() navigateToMainActivity()
} }

View File

@ -1,5 +1,6 @@
package net.dankito.banking.ui.android.authentication package net.dankito.banking.ui.android.authentication
import at.favre.lib.crypto.bcrypt.BCrypt
import net.dankito.banking.persistence.IBankingPersistence import net.dankito.banking.persistence.IBankingPersistence
import net.dankito.banking.util.ISerializer import net.dankito.banking.util.ISerializer
import net.dankito.utils.multiplatform.File import net.dankito.utils.multiplatform.File
@ -14,8 +15,6 @@ open class AuthenticationService(
) { ) {
companion object { companion object {
private const val AuthenticationTypeFilename = "a"
private const val AuthenticationSettingsFilename = "s" private const val AuthenticationSettingsFilename = "s"
private val log = LoggerFactory.getLogger(AuthenticationService::class.java) private val log = LoggerFactory.getLogger(AuthenticationService::class.java)
@ -30,130 +29,85 @@ open class AuthenticationService(
init { init {
authenticationType = loadAuthenticationType() val settings = loadAuthenticationSettings()
if (authenticationType == AuthenticationType.None) { if (settings == null) { // first app run -> create a default password
val authenticationSettings = loadAuthenticationSettings()
if (authenticationSettings == null) { // first app run -> create a default password
removeAppProtection() removeAppProtection()
} }
else { else {
openDatabase(authenticationSettings) authenticationType = settings.type
if (settings.type == AuthenticationType.None) {
openDatabase(settings)
} }
} }
} }
open fun userLoggedInWithBiometricAuthentication() { open fun authenticateUserWithPassword(enteredPassword: String): Boolean {
loadAuthenticationSettings()?.let { if (isCorrectUserPassword(enteredPassword)) {
openDatabase(it) return openDatabase(enteredPassword)
}
} }
open fun userLoggedInWithPassword(enteredPassword: String) { return false
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) { protected open fun openDatabase(authenticationSettings: AuthenticationSettings) {
openDatabase(authenticationSettings.userPassword) openDatabase(authenticationSettings.userPassword)
} }
protected open fun openDatabase(password: String?) { protected open fun openDatabase(password: String?): Boolean {
persistence.decryptData(password) return persistence.decryptData(password)
} }
open fun setAuthenticationMethodToBiometric() { open fun setAuthenticationMethodToBiometric() {
if (saveNewUserPassword(generateRandomPassword())) { saveNewAuthenticationMethod(AuthenticationType.Biometric, generateRandomPassword())
if (saveAuthenticationType(AuthenticationType.Biometric)) {
authenticationType = AuthenticationType.Biometric
}
}
} }
open fun setAuthenticationMethodToPassword(newPassword: String) { open fun setAuthenticationMethodToPassword(newPassword: String) {
if (saveNewUserPassword(newPassword)) { saveNewAuthenticationMethod(AuthenticationType.Password, newPassword)
if (saveAuthenticationType(AuthenticationType.Password)) {
authenticationType = AuthenticationType.Password
}
}
} }
open fun removeAppProtection() { open fun removeAppProtection() {
if (saveNewUserPassword(generateRandomPassword())) { saveNewAuthenticationMethod(AuthenticationType.None, generateRandomPassword())
if (saveAuthenticationType(AuthenticationType.None)) {
authenticationType = AuthenticationType.None
}
}
} }
open fun isCorrectUserPassword(password: String): Boolean { protected open fun saveNewAuthenticationMethod(type: AuthenticationType, newPassword: 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 {
val settings = loadOrCreateDefaultAuthenticationSettings() val settings = loadOrCreateDefaultAuthenticationSettings()
val currentPassword = settings.userPassword
if (currentPassword != newPassword) { if (type == settings.type &&
settings.userPassword = newPassword ((type != AuthenticationType.Password && settings.userPassword == newPassword)
|| (type == AuthenticationType.Password && isCorrectUserPassword(newPassword)))) { // nothing changed
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)) { 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 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 true
} }
return false return false
} }
return true
}
protected open fun loadOrCreateDefaultAuthenticationSettings(): AuthenticationSettings { protected open fun loadOrCreateDefaultAuthenticationSettings(): AuthenticationSettings {
return loadAuthenticationSettings() ?: AuthenticationSettings(null) return loadAuthenticationSettings() ?: AuthenticationSettings(AuthenticationType.None)
} }
protected open fun loadAuthenticationSettings(): AuthenticationSettings? { protected open fun loadAuthenticationSettings(): AuthenticationSettings? {

View File

@ -2,5 +2,11 @@ package net.dankito.banking.ui.android.authentication
open class AuthenticationSettings( open class AuthenticationSettings(
var userPassword: String? = null open var type: AuthenticationType,
) open var hashedUserPassword: String? = null,
open var userPassword: String? = null
) {
internal constructor() : this(AuthenticationType.None) // for object deserializers
}

View File

@ -2,11 +2,15 @@ package net.dankito.banking.persistence
import net.dankito.banking.ui.model.* import net.dankito.banking.ui.model.*
import net.dankito.banking.ui.model.settings.AppSettings import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.utils.multiplatform.File
interface IBankingPersistence { interface IBankingPersistence {
fun decryptData(password: String?): Boolean
fun changePassword(newPassword: String?)
fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>)
fun deleteBank(bank: TypedBankData, allBanks: List<TypedBankData>) fun deleteBank(bank: TypedBankData, allBanks: List<TypedBankData>)

View File

@ -6,6 +6,15 @@ import net.dankito.banking.ui.model.settings.AppSettings
open class NoOpBankingPersistence : IBankingPersistence { open class NoOpBankingPersistence : IBankingPersistence {
override fun decryptData(password: String?): Boolean {
return true
}
override fun changePassword(newPassword: String?) {
}
override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) { override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
} }