Keeping passwords now only as CharArray in memory as it may takes a long time till a String gets garbage collected

This commit is contained in:
dankito 2020-10-13 18:24:40 +02:00
parent bc853b03e3
commit 8b2a210269
10 changed files with 66 additions and 41 deletions

View File

@ -8,6 +8,11 @@ val Char.isUpperCase: Boolean
get() = isLowerCase == false get() = isLowerCase == false
fun CharArray.asString(): String {
return this.joinToString("")
}
fun Date.isBefore(other: Date): Boolean { fun Date.isBefore(other: Date): Boolean {
return compareTo(other) < 0 return compareTo(other) < 0
} }

View File

@ -14,6 +14,7 @@ import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.banking.ui.model.tan.MobilePhoneTanMedium import net.dankito.banking.ui.model.tan.MobilePhoneTanMedium
import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
import net.dankito.banking.util.persistence.downloadIcon import net.dankito.banking.util.persistence.downloadIcon
import net.dankito.utils.multiplatform.asString
import net.sqlcipher.database.SQLiteDatabase import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactory import net.sqlcipher.database.SupportFactory
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -37,13 +38,13 @@ open class RoomBankingPersistence(protected open val applicationContext: Context
protected lateinit var database: BankingDatabase protected lateinit var database: BankingDatabase
override fun decryptData(password: String?): Boolean { override fun decryptData(password: CharArray): Boolean {
return openDatabase(password) return openDatabase(password)
} }
override fun changePassword(newPassword: String?): Boolean { override fun changePassword(newPassword: CharArray): Boolean {
if (this::database.isInitialized) { if (this::database.isInitialized) {
val cursor = database.query("PRAGMA rekey = '$newPassword';", emptyArray()) val cursor = database.query("PRAGMA rekey = '${newPassword.asString()}';", emptyArray())
return cursor.count == 1 // TODO: also check if first column content is 'ok' ? return cursor.count == 1 // TODO: also check if first column content is 'ok' ?
} }
@ -52,9 +53,9 @@ open class RoomBankingPersistence(protected open val applicationContext: Context
} }
} }
protected open fun openDatabase(password: String?): Boolean { protected open fun openDatabase(password: CharArray): Boolean {
try { try {
val passphrase = password?.let { SQLiteDatabase.getBytes(password.toCharArray()) } ?: ByteArray(0) val passphrase = SQLiteDatabase.getBytes(password)
val factory = SupportFactory(passphrase) val factory = SupportFactory(passphrase)
database = Room.databaseBuilder(applicationContext, BankingDatabase::class.java, DatabaseName) database = Room.databaseBuilder(applicationContext, BankingDatabase::class.java, DatabaseName)

View File

@ -35,12 +35,12 @@ open class BankingPersistenceJson(
} }
override fun decryptData(password: String?): Boolean { override fun decryptData(password: CharArray): Boolean {
// TODO: may implement data decryption. But then we have to store password to be able to encrypt data // TODO: may implement data decryption. But then we have to store password to be able to encrypt data
return true return true
} }
override fun changePassword(newPassword: String?): Boolean { override fun changePassword(newPassword: CharArray): Boolean {
// TODO: may implement data decryption. But then we have to store newPassword to be able to encrypt data // TODO: may implement data decryption. But then we have to store newPassword to be able to encrypt data
return true return true
} }

View File

@ -67,9 +67,7 @@ open class LoginActivity : BaseActivity() {
protected open fun checkEnteredPasswordAndLogIn() { protected open fun checkEnteredPasswordAndLogIn() {
btnLogin.isEnabled = false btnLogin.isEnabled = false
val enteredPassword = edtxtLoginPassword.text if (authenticationService.authenticateUserWithPassword(edtxtLoginPassword.chars)) {
if (authenticationService.authenticateUserWithPassword(enteredPassword)) {
navigateToMainActivity() navigateToMainActivity()
} }
else { else {

View File

@ -6,6 +6,7 @@ import net.dankito.banking.persistence.IBankingPersistence
import net.dankito.banking.ui.android.security.CryptographyManager import net.dankito.banking.ui.android.security.CryptographyManager
import net.dankito.banking.util.ISerializer import net.dankito.banking.util.ISerializer
import net.dankito.utils.multiplatform.File import net.dankito.utils.multiplatform.File
import net.dankito.utils.multiplatform.asString
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.crypto.Cipher import javax.crypto.Cipher
@ -54,7 +55,7 @@ open class AuthenticationService(
} }
open fun authenticateUserWithPassword(enteredPassword: String): Boolean { open fun authenticateUserWithPassword(enteredPassword: CharArray): Boolean {
if (isCorrectUserPassword(enteredPassword)) { if (isCorrectUserPassword(enteredPassword)) {
loadAuthenticationSettings()?.let { settings -> loadAuthenticationSettings()?.let { settings ->
return openDatabase(settings, enteredPassword) return openDatabase(settings, enteredPassword)
@ -64,9 +65,9 @@ open class AuthenticationService(
return false return false
} }
open fun isCorrectUserPassword(enteredPassword: String): Boolean { open fun isCorrectUserPassword(enteredPassword: CharArray): Boolean {
loadAuthenticationSettings()?.let { settings -> loadAuthenticationSettings()?.let { settings ->
val result = BCrypt.verifyer().verify(enteredPassword.toCharArray(), settings.hashedUserPassword) val result = BCrypt.verifyer().verify(enteredPassword, settings.hashedUserPassword)
return result.verified return result.verified
} }
@ -119,7 +120,7 @@ open class AuthenticationService(
?: run { result(false) } ?: run { result(false) }
} }
protected open fun openDatabase(settings: AuthenticationSettings, userPassword: String? = null): Boolean { protected open fun openDatabase(settings: AuthenticationSettings, userPassword: CharArray? = null): Boolean {
settings.defaultPassword?.let { encryptedPassword -> settings.defaultPassword?.let { encryptedPassword ->
settings.initializationVector?.let { iv -> settings.initializationVector?.let { iv ->
settings.salt?.let { salt -> settings.salt?.let { salt ->
@ -127,7 +128,7 @@ open class AuthenticationService(
decodeFromBase64(iv), decodeFromBase64(salt)) decodeFromBase64(iv), decodeFromBase64(salt))
if (userPassword != null) { if (userPassword != null) {
return openDatabase(userPassword + "_" + defaultPassword) return openDatabase(concatPasswords(userPassword, defaultPassword))
} }
else { else {
return openDatabase(defaultPassword) return openDatabase(defaultPassword)
@ -139,7 +140,7 @@ open class AuthenticationService(
return false return false
} }
protected open fun openDatabase(password: String?): Boolean { protected open fun openDatabase(password: CharArray): Boolean {
return persistence.decryptData(password) return persistence.decryptData(password)
} }
@ -148,7 +149,7 @@ open class AuthenticationService(
saveNewAuthenticationMethod(AuthenticationType.Biometric, null) saveNewAuthenticationMethod(AuthenticationType.Biometric, null)
} }
open fun setAuthenticationMethodToPassword(newPassword: String) { open fun setAuthenticationMethodToPassword(newPassword: CharArray) {
saveNewAuthenticationMethod(AuthenticationType.Password, newPassword) saveNewAuthenticationMethod(AuthenticationType.Password, newPassword)
} }
@ -157,7 +158,7 @@ open class AuthenticationService(
} }
protected open fun saveNewAuthenticationMethod(type: AuthenticationType, newUserPassword: String?): Boolean { protected open fun saveNewAuthenticationMethod(type: AuthenticationType, newUserPassword: CharArray?): Boolean {
val settings = loadOrCreateDefaultAuthenticationSettings() val settings = loadOrCreateDefaultAuthenticationSettings()
val newDefaultPassword = generateRandomPassword() val newDefaultPassword = generateRandomPassword()
var newDatabasePassword = newDefaultPassword var newDatabasePassword = newDefaultPassword
@ -182,8 +183,8 @@ open class AuthenticationService(
settings.salt = encodeToBase64(salt) settings.salt = encodeToBase64(salt)
if (newUserPassword != null) { if (newUserPassword != null) {
settings.hashedUserPassword = BCrypt.withDefaults().hashToString(12, newUserPassword.toCharArray()) settings.hashedUserPassword = BCrypt.withDefaults().hashToString(12, newUserPassword)
newDatabasePassword = newUserPassword + "_" + newDefaultPassword newDatabasePassword = concatPasswords(newUserPassword, newDefaultPassword)
} }
} }
@ -199,6 +200,16 @@ open class AuthenticationService(
return false return false
} }
protected open fun concatPasswords(userPassword: CharArray, defaultPassword: CharArray): CharArray {
val concatenated = StringBuilder(userPassword.size + defaultPassword.size + 1)
concatenated.append(userPassword)
concatenated.append("_")
concatenated.append(defaultPassword)
return concatenated.toList().toCharArray()
}
protected open fun loadOrCreateDefaultAuthenticationSettings(): AuthenticationSettings { protected open fun loadOrCreateDefaultAuthenticationSettings(): AuthenticationSettings {
return loadAuthenticationSettings() ?: AuthenticationSettings(AuthenticationType.None) return loadAuthenticationSettings() ?: AuthenticationSettings(AuthenticationType.None)
} }
@ -236,19 +247,19 @@ open class AuthenticationService(
} }
open fun generateRandomPassword(): String { open fun generateRandomPassword(): CharArray {
return generateRandomPassword(30) return generateRandomPassword(30)
} }
open fun generateRandomPassword(passwordLength: Int): String { open fun generateRandomPassword(passwordLength: Int): CharArray {
val dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789§±!@#$%^&*-_=+;:|/?.>,<" val dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789§±!@#$%^&*-_=+;:|/?.>,<"
val passwordBuilder = StringBuilder() val password = CharArray(passwordLength)
IntRange(0, passwordLength).forEach { IntRange(0, passwordLength - 1).forEach { index ->
passwordBuilder.append(dictionary.random()) password[index] = dictionary.random()
} }
return passwordBuilder.toString() return password
} }

View File

@ -160,7 +160,7 @@ open class ProtectAppSettingsDialog : SettingsDialogBase() {
protected open fun setAuthenticationMethod() { protected open fun setAuthenticationMethod() {
when { when {
btnShowPasswordAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToPassword(edtxtPassword.text) btnShowPasswordAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToPassword(edtxtPassword.chars)
btnShowBiometricAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToBiometric() btnShowBiometricAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToBiometric()
btnShowRemoveAppProtectionSection.isChecked -> authenticationService.removeAppProtection() btnShowRemoveAppProtectionSection.isChecked -> authenticationService.removeAppProtection()
else -> log.error("This should never occur! Has there a new authentication method been added?") else -> log.error("This should never occur! Has there a new authentication method been added?")

View File

@ -23,8 +23,6 @@ open class CryptographyManager {
const val AndroidKeyStore = "AndroidKeyStore" const val AndroidKeyStore = "AndroidKeyStore"
val PasswordCharset = Charsets.UTF_8
private const val KeySize: Int = 256 private const val KeySize: Int = 256
private const val EncryptionBlockMode = KeyProperties.BLOCK_MODE_GCM private const val EncryptionBlockMode = KeyProperties.BLOCK_MODE_GCM
private const val EncryptionPadding = KeyProperties.ENCRYPTION_PADDING_NONE private const val EncryptionPadding = KeyProperties.ENCRYPTION_PADDING_NONE
@ -78,33 +76,33 @@ open class CryptographyManager {
} }
open fun encryptData(plaintext: String, cipher: Cipher): ByteArray { open fun encryptData(plaintext: CharArray, cipher: Cipher): ByteArray {
return cipher.doFinal(plaintext.toByteArray(PasswordCharset)) return cipher.doFinal(mapToBytes(plaintext))
} }
open fun decryptData(cipherText: ByteArray, cipher: Cipher): String { open fun decryptData(cipherText: ByteArray, cipher: Cipher): CharArray {
val plainTextBytes = cipher.doFinal(cipherText) val plainTextBytes = cipher.doFinal(cipherText)
return String(plainTextBytes, PasswordCharset) return mapToChars(plainTextBytes)
} }
open fun encryptDataWithPbe(plaintext: String, password: String, salt: ByteArray): Pair<ByteArray, ByteArray> { open fun encryptDataWithPbe(plaintext: CharArray, password: String, salt: ByteArray): Pair<ByteArray, ByteArray> {
val secret: SecretKey = generatePbeSecretKey(password, salt) val secret: SecretKey = generatePbeSecretKey(password, salt)
val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding") val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secret) cipher.init(Cipher.ENCRYPT_MODE, secret)
val initializationVector = cipher.iv val initializationVector = cipher.iv
return Pair(cipher.doFinal(plaintext.toByteArray(PasswordCharset)), initializationVector) return Pair(cipher.doFinal(mapToBytes(plaintext)), initializationVector)
} }
open fun decryptDataWithPbe(cipherText: ByteArray, password: String, initializationVector: ByteArray, salt: ByteArray): String { open fun decryptDataWithPbe(cipherText: ByteArray, password: String, initializationVector: ByteArray, salt: ByteArray): CharArray {
val secret: SecretKey = generatePbeSecretKey(password, salt) val secret: SecretKey = generatePbeSecretKey(password, salt)
val cipher = Cipher.getInstance("AES/GCM/NoPadding") val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secret, IvParameterSpec(initializationVector)) cipher.init(Cipher.DECRYPT_MODE, secret, IvParameterSpec(initializationVector))
val plainTextBytes = cipher.doFinal(cipherText) val plainTextBytes = cipher.doFinal(cipherText)
return String(plainTextBytes, PasswordCharset) return mapToChars(plainTextBytes)
} }
protected open fun generatePbeSecretKey(userPassword: String, salt: ByteArray): SecretKey { protected open fun generatePbeSecretKey(userPassword: String, salt: ByteArray): SecretKey {
@ -127,6 +125,15 @@ open class CryptographyManager {
} }
protected open fun mapToBytes(chars: CharArray): ByteArray {
return chars.map { it.toByte() }.toByteArray()
}
protected open fun mapToChars(plainTextBytes: ByteArray): CharArray {
return plainTextBytes.map { it.toChar() }.toCharArray()
}
open fun findBestPbeAlgorithm(): String? { open fun findBestPbeAlgorithm(): String? {
return findBestMatchingAlgorithm(SecurityProviderServiceType.SecretKeyFactory, "PBKDF2","PBKDF2WithHmacSHA256") return findBestMatchingAlgorithm(SecurityProviderServiceType.SecretKeyFactory, "PBKDF2","PBKDF2WithHmacSHA256")
?: findBestMatchingAlgorithm(SecurityProviderServiceType.SecretKeyFactory, "PBE") ?: findBestMatchingAlgorithm(SecurityProviderServiceType.SecretKeyFactory, "PBE")

View File

@ -52,6 +52,9 @@ open class FormEditText @JvmOverloads constructor(
get() = textInputEditText.textString get() = textInputEditText.textString
set(value) = textInputEditText.setText(value) set(value) = textInputEditText.setText(value)
open val chars: CharArray
get() = actualEditText.text.toList().toCharArray()
open val actualEditText: EditText open val actualEditText: EditText
get() = textInputEditText get() = textInputEditText

View File

@ -6,9 +6,9 @@ import net.dankito.banking.ui.model.settings.AppSettings
interface IBankingPersistence { interface IBankingPersistence {
fun decryptData(password: String?): Boolean fun decryptData(password: CharArray): Boolean
fun changePassword(newPassword: String?): Boolean fun changePassword(newPassword: CharArray): Boolean
fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>)

View File

@ -6,11 +6,11 @@ import net.dankito.banking.ui.model.settings.AppSettings
open class NoOpBankingPersistence : IBankingPersistence { open class NoOpBankingPersistence : IBankingPersistence {
override fun decryptData(password: String?): Boolean { override fun decryptData(password: CharArray): Boolean {
return true return true
} }
override fun changePassword(newPassword: String?): Boolean { override fun changePassword(newPassword: CharArray): Boolean {
return true return true
} }