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"
bcryptVersion = "0.9.0"
/* JavaFX */

View File

@ -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<BankAccount>()
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<TanMethod>()
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<TypedBankData>) {
(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>())
db.tanMediumDao().delete(bank.tanMedia.filterIsInstance<TanMedium>())
database.tanMethodDao().delete(bank.supportedTanMethods.filterIsInstance<TanMethod>())
database.tanMediumDao().delete(bank.tanMedia.filterIsInstance<TanMedium>())
db.bankDao().delete(bank)
database.bankDao().delete(bank)
}
}
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 ->
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<TransactionParty> {
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) }

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>) {
saveAllBanks(allBanks)
}

View File

@ -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"

View File

@ -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()
}

View File

@ -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? {

View File

@ -2,5 +2,11 @@ package net.dankito.banking.ui.android.authentication
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.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<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 {
override fun decryptData(password: String?): Boolean {
return true
}
override fun changePassword(newPassword: String?) {
}
override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
}