diff --git a/common/src/commonMain/kotlin/net/dankito/utils/multiplatform/CommonExtensions.kt b/common/src/commonMain/kotlin/net/dankito/utils/multiplatform/CommonExtensions.kt index 4a6f98f1..f8152c6b 100644 --- a/common/src/commonMain/kotlin/net/dankito/utils/multiplatform/CommonExtensions.kt +++ b/common/src/commonMain/kotlin/net/dankito/utils/multiplatform/CommonExtensions.kt @@ -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 } 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 8d8115af..8a6bf1e0 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 @@ -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) 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 ff503b12..c1300d4d 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,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 } 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 87eb715e..2f689197 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 @@ -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 { 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 f68c4664..f62ddbe4 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 @@ -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 } diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/ProtectAppSettingsDialog.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/ProtectAppSettingsDialog.kt index 3ade3659..666b79c6 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/ProtectAppSettingsDialog.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/ProtectAppSettingsDialog.kt @@ -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?") diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/security/CryptographyManager.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/security/CryptographyManager.kt index 94a4e795..5822839a 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/security/CryptographyManager.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/security/CryptographyManager.kt @@ -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 { + open fun encryptDataWithPbe(plaintext: CharArray, password: String, salt: ByteArray): Pair { 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") diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt index b01bc3b3..b1ce4a8b 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt @@ -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 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 793067c0..a7ed4632 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 @@ -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) 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 da816fda..d9b10e32 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,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 }