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
fun CharArray.asString(): String {
return this.joinToString("")
}
fun Date.isBefore(other: Date): Boolean {
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.TanGeneratorTanMedium
import net.dankito.banking.util.persistence.downloadIcon
import net.dankito.utils.multiplatform.asString
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactory
import org.slf4j.LoggerFactory
@ -37,13 +38,13 @@ open class RoomBankingPersistence(protected open val applicationContext: Context
protected lateinit var database: BankingDatabase
override fun decryptData(password: String?): Boolean {
override fun decryptData(password: CharArray): Boolean {
return openDatabase(password)
}
override fun changePassword(newPassword: String?): Boolean {
override fun changePassword(newPassword: CharArray): Boolean {
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' ?
}
@ -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 {
val passphrase = password?.let { SQLiteDatabase.getBytes(password.toCharArray()) } ?: ByteArray(0)
val passphrase = SQLiteDatabase.getBytes(password)
val factory = SupportFactory(passphrase)
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
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
return true
}

View File

@ -67,9 +67,7 @@ open class LoginActivity : BaseActivity() {
protected open fun checkEnteredPasswordAndLogIn() {
btnLogin.isEnabled = false
val enteredPassword = edtxtLoginPassword.text
if (authenticationService.authenticateUserWithPassword(enteredPassword)) {
if (authenticationService.authenticateUserWithPassword(edtxtLoginPassword.chars)) {
navigateToMainActivity()
}
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.util.ISerializer
import net.dankito.utils.multiplatform.File
import net.dankito.utils.multiplatform.asString
import org.slf4j.LoggerFactory
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)) {
loadAuthenticationSettings()?.let { settings ->
return openDatabase(settings, enteredPassword)
@ -64,9 +65,9 @@ open class AuthenticationService(
return false
}
open fun isCorrectUserPassword(enteredPassword: String): Boolean {
open fun isCorrectUserPassword(enteredPassword: CharArray): Boolean {
loadAuthenticationSettings()?.let { settings ->
val result = BCrypt.verifyer().verify(enteredPassword.toCharArray(), settings.hashedUserPassword)
val result = BCrypt.verifyer().verify(enteredPassword, settings.hashedUserPassword)
return result.verified
}
@ -119,7 +120,7 @@ open class AuthenticationService(
?: 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.initializationVector?.let { iv ->
settings.salt?.let { salt ->
@ -127,7 +128,7 @@ open class AuthenticationService(
decodeFromBase64(iv), decodeFromBase64(salt))
if (userPassword != null) {
return openDatabase(userPassword + "_" + defaultPassword)
return openDatabase(concatPasswords(userPassword, defaultPassword))
}
else {
return openDatabase(defaultPassword)
@ -139,7 +140,7 @@ open class AuthenticationService(
return false
}
protected open fun openDatabase(password: String?): Boolean {
protected open fun openDatabase(password: CharArray): Boolean {
return persistence.decryptData(password)
}
@ -148,7 +149,7 @@ open class AuthenticationService(
saveNewAuthenticationMethod(AuthenticationType.Biometric, null)
}
open fun setAuthenticationMethodToPassword(newPassword: String) {
open fun setAuthenticationMethodToPassword(newPassword: CharArray) {
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 newDefaultPassword = generateRandomPassword()
var newDatabasePassword = newDefaultPassword
@ -182,8 +183,8 @@ open class AuthenticationService(
settings.salt = encodeToBase64(salt)
if (newUserPassword != null) {
settings.hashedUserPassword = BCrypt.withDefaults().hashToString(12, newUserPassword.toCharArray())
newDatabasePassword = newUserPassword + "_" + newDefaultPassword
settings.hashedUserPassword = BCrypt.withDefaults().hashToString(12, newUserPassword)
newDatabasePassword = concatPasswords(newUserPassword, newDefaultPassword)
}
}
@ -199,6 +200,16 @@ open class AuthenticationService(
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 {
return loadAuthenticationSettings() ?: AuthenticationSettings(AuthenticationType.None)
}
@ -236,19 +247,19 @@ open class AuthenticationService(
}
open fun generateRandomPassword(): String {
open fun generateRandomPassword(): CharArray {
return generateRandomPassword(30)
}
open fun generateRandomPassword(passwordLength: Int): String {
open fun generateRandomPassword(passwordLength: Int): CharArray {
val dictionary = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789§±!@#$%^&*-_=+;:|/?.>,<"
val passwordBuilder = StringBuilder()
IntRange(0, passwordLength).forEach {
passwordBuilder.append(dictionary.random())
val password = CharArray(passwordLength)
IntRange(0, passwordLength - 1).forEach { index ->
password[index] = dictionary.random()
}
return passwordBuilder.toString()
return password
}

View File

@ -160,7 +160,7 @@ open class ProtectAppSettingsDialog : SettingsDialogBase() {
protected open fun setAuthenticationMethod() {
when {
btnShowPasswordAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToPassword(edtxtPassword.text)
btnShowPasswordAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToPassword(edtxtPassword.chars)
btnShowBiometricAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToBiometric()
btnShowRemoveAppProtectionSection.isChecked -> authenticationService.removeAppProtection()
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"
val PasswordCharset = Charsets.UTF_8
private const val KeySize: Int = 256
private const val EncryptionBlockMode = KeyProperties.BLOCK_MODE_GCM
private const val EncryptionPadding = KeyProperties.ENCRYPTION_PADDING_NONE
@ -78,33 +76,33 @@ open class CryptographyManager {
}
open fun encryptData(plaintext: String, cipher: Cipher): ByteArray {
return cipher.doFinal(plaintext.toByteArray(PasswordCharset))
open fun encryptData(plaintext: CharArray, cipher: Cipher): ByteArray {
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)
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 cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secret)
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 cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secret, IvParameterSpec(initializationVector))
val plainTextBytes = cipher.doFinal(cipherText)
return String(plainTextBytes, PasswordCharset)
return mapToChars(plainTextBytes)
}
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? {
return findBestMatchingAlgorithm(SecurityProviderServiceType.SecretKeyFactory, "PBKDF2","PBKDF2WithHmacSHA256")
?: findBestMatchingAlgorithm(SecurityProviderServiceType.SecretKeyFactory, "PBE")

View File

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

View File

@ -6,9 +6,9 @@ import net.dankito.banking.ui.model.settings.AppSettings
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>)

View File

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