From 4e27bffd096dd51e3a04a2b16cf575cd52db9cef Mon Sep 17 00:00:00 2001 From: dankito Date: Thu, 8 Oct 2020 01:14:14 +0200 Subject: [PATCH] Implemented encrypting default password if no app protection is set --- .../authentication/AuthenticationService.kt | 38 +++++++++----- .../authentication/AuthenticationSettings.kt | 6 +-- .../settings/ProtectAppSettingsDialog.kt | 8 ++- .../android/security/CryptographyManager.kt | 49 ++++++++++++++++++- 4 files changed, 83 insertions(+), 18 deletions(-) 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 9920ad82..fd897fec 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 @@ -19,10 +19,12 @@ open class AuthenticationService( ) { companion object { - private const val AuthenticationSettingsFilename = "s" + private const val AuthenticationSettingsFilename = "a" private const val EncryptionKeyName = "BankingAndroidKey" + private const val DefaultPasswordEncryptionKey = "AnyData" // TODO: store in a secure place + private val log = LoggerFactory.getLogger(AuthenticationService::class.java) } @@ -89,7 +91,7 @@ open class AuthenticationService( val cipher = cryptographyManager.getInitializedCipherForDecryption(EncryptionKeyName, iv) biometricAuthenticationService.authenticate(cipher) { authenticationResult -> if (authenticationResult.successful) { - settings.encryptedUserPassword?.let { + settings.encryptedDefaultPassword?.let { val encryptedUserPassword = decodeFromBase64(it) val decrypted = cryptographyManager.decryptData(encryptedUserPassword, cipher) @@ -104,8 +106,19 @@ open class AuthenticationService( ?: run { result(false) } } - protected open fun openDatabase(authenticationSettings: AuthenticationSettings) { - openDatabase(authenticationSettings.userPassword) + protected open fun openDatabase(settings: AuthenticationSettings) { + if (settings.type == AuthenticationType.None) { + settings.encryptedDefaultPassword?.let { encryptedPassword -> + settings.initializationVector?.let { iv -> + settings.salt?.let { salt -> + val decrypted = cryptographyManager.decryptDataWithPbe(decodeFromBase64(encryptedPassword), DefaultPasswordEncryptionKey, + decodeFromBase64(iv), decodeFromBase64(salt)) + + openDatabase(decrypted) + } + } + } + } } protected open fun openDatabase(password: String?): Boolean { @@ -129,22 +142,25 @@ open class AuthenticationService( protected open fun saveNewAuthenticationMethod(type: AuthenticationType, newPassword: String): Boolean { val settings = loadOrCreateDefaultAuthenticationSettings() - if (type == settings.type && - ((type == AuthenticationType.Password && isCorrectUserPassword(newPassword)) || settings.userPassword == 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.None) newPassword else null + settings.initializationVector = null + settings.salt = null if (type == AuthenticationType.Biometric) { encryptionCipherForBiometric?.let { encryptionCipher -> val encryptedPassword = cryptographyManager.encryptData(newPassword, encryptionCipher) - settings.encryptedUserPassword = encodeToBase64(encryptedPassword) + settings.encryptedDefaultPassword = encodeToBase64(encryptedPassword) settings.initializationVector = encodeToBase64(encryptionCipher.iv) } } + else if (type == AuthenticationType.None) { + val salt = cryptographyManager.generateRandomBytes(8) + val (encryptedPassword, iv) = cryptographyManager.encryptDataWithPbe(newPassword, DefaultPasswordEncryptionKey, salt) + settings.encryptedDefaultPassword = encodeToBase64(encryptedPassword) + settings.initializationVector = encodeToBase64(iv) + settings.salt = encodeToBase64(salt) + } if (saveAuthenticationSettings(settings)) { this.authenticationType = type diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt index e3596e3f..b6d59991 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationSettings.kt @@ -4,9 +4,9 @@ package net.dankito.banking.ui.android.authentication open class AuthenticationSettings( open var type: AuthenticationType, open var hashedUserPassword: String? = null, - open var userPassword: String? = null, - open var encryptedUserPassword: String? = null, - open var initializationVector: String? = null + open var encryptedDefaultPassword: String? = null, + open var initializationVector: String? = null, + open var salt: String? = null ) { internal constructor() : this(AuthenticationType.None) // for object deserializers 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 76563801..ef487502 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 @@ -87,7 +87,11 @@ open class ProtectAppSettingsDialog : SettingsDialogBase() { } } - btnBiometricAuthentication.authenticationSuccessful = { btnSetAuthenticationMethod.isEnabled = true } + btnBiometricAuthentication.customButtonClickHandler = { + authenticationService.authenticateUserWithBiometricToSetAsNewAuthenticationMethod { result -> + btnSetAuthenticationMethod.isEnabled = result.successful + } + } edtxtPassword.actualEditText.addTextChangedListener(StandardTextWatcher { checkIfEnteredPasswordsMatch() } ) edtxtPassword.actualEditText.addEnterPressedListener { checkIfEnteredPasswordsMatchAndSetAuthenticationMethod() } @@ -153,8 +157,8 @@ open class ProtectAppSettingsDialog : SettingsDialogBase() { protected open fun setAuthenticationMethod() { when { - btnShowBiometricAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToBiometric() btnShowPasswordAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToPassword(edtxtPassword.text) + 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 ad786a87..4e0a90a1 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 @@ -1,12 +1,18 @@ package net.dankito.banking.ui.android.security +import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import java.security.KeyStore +import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec open class CryptographyManager { @@ -50,8 +56,8 @@ open class CryptographyManager { } open fun decryptData(cipherText: ByteArray, cipher: Cipher): String { - val plaintext = cipher.doFinal(cipherText) - return String(plaintext, PasswordCharset) + val plainTextBytes = cipher.doFinal(cipherText) + return String(plainTextBytes, PasswordCharset) } protected open fun getOrCreateSecretKey(keyName: String): SecretKey { @@ -75,4 +81,43 @@ open class CryptographyManager { return keyGenerator.generateKey() } + + open fun encryptDataWithPbe(plaintext: String, 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) + } + + open fun decryptDataWithPbe(cipherText: ByteArray, password: String, initializationVector: ByteArray, salt: ByteArray): String { + 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) + } + + protected open fun generatePbeSecretKey(userPassword: String, salt: ByteArray): SecretKey { + // Initialize PBE with password + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val spec = PBEKeySpec(userPassword.toCharArray(), salt, 65536, 256) + val key = factory.generateSecret(spec) + + return SecretKeySpec(key.encoded, "AES") + } + + open fun generateRandomBytes(countBytes: Int): ByteArray { + return ByteArray(countBytes).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + SecureRandom.getInstanceStrong().nextBytes(this) + } else { + SecureRandom().nextBytes(this) + } + } + } + } \ No newline at end of file