Implemented encrypting biometric password
This commit is contained in:
parent
32c71fcb39
commit
2cab245600
|
@ -4,6 +4,8 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import kotlinx.android.synthetic.main.activity_login.*
|
import kotlinx.android.synthetic.main.activity_login.*
|
||||||
|
import kotlinx.android.synthetic.main.activity_login.btnBiometricAuthentication
|
||||||
|
import kotlinx.android.synthetic.main.view_biometric_authentication_button.*
|
||||||
import net.dankito.banking.ui.android.MainActivity
|
import net.dankito.banking.ui.android.MainActivity
|
||||||
import net.dankito.banking.ui.android.R
|
import net.dankito.banking.ui.android.R
|
||||||
import net.dankito.banking.ui.android.authentication.AuthenticationService
|
import net.dankito.banking.ui.android.authentication.AuthenticationService
|
||||||
|
@ -47,7 +49,15 @@ open class LoginActivity : BaseActivity() {
|
||||||
else {
|
else {
|
||||||
lytPasswordAuthentication.visibility = View.GONE
|
lytPasswordAuthentication.visibility = View.GONE
|
||||||
|
|
||||||
btnBiometricAuthentication.authenticationSuccessful = { biometricAuthenticationSuccessful() }
|
btnBiometricAuthentication.customButtonClickHandler = {
|
||||||
|
authenticationService.authenticateUserWithBiometric { result ->
|
||||||
|
if (result) {
|
||||||
|
btnStartBiometricAuthentication.isEnabled = false
|
||||||
|
|
||||||
|
biometricAuthenticationSuccessful()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
btnBiometricAuthentication.showBiometricPrompt()
|
btnBiometricAuthentication.showBiometricPrompt()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
package net.dankito.banking.ui.android.authentication
|
package net.dankito.banking.ui.android.authentication
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
import at.favre.lib.crypto.bcrypt.BCrypt
|
import at.favre.lib.crypto.bcrypt.BCrypt
|
||||||
import net.dankito.banking.persistence.IBankingPersistence
|
import net.dankito.banking.persistence.IBankingPersistence
|
||||||
|
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 org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
|
||||||
open class AuthenticationService(
|
open class AuthenticationService(
|
||||||
protected open val biometricAuthenticationService: IBiometricAuthenticationService,
|
protected open val biometricAuthenticationService: IBiometricAuthenticationService,
|
||||||
protected open val persistence: IBankingPersistence,
|
protected open val persistence: IBankingPersistence,
|
||||||
protected open val dataFolder: File,
|
protected open val dataFolder: File,
|
||||||
protected open val serializer: ISerializer
|
protected open val serializer: ISerializer,
|
||||||
|
protected open val cryptographyManager: CryptographyManager = CryptographyManager()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val AuthenticationSettingsFilename = "s"
|
private const val AuthenticationSettingsFilename = "s"
|
||||||
|
|
||||||
|
private const val EncryptionKeyName = "BankingAndroidKey"
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(AuthenticationService::class.java)
|
private val log = LoggerFactory.getLogger(AuthenticationService::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +33,8 @@ open class AuthenticationService(
|
||||||
open var authenticationType: AuthenticationType = AuthenticationType.None
|
open var authenticationType: AuthenticationType = AuthenticationType.None
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
|
protected open var encryptionCipherForBiometric: Cipher? = null
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val settings = loadAuthenticationSettings()
|
val settings = loadAuthenticationSettings()
|
||||||
|
@ -62,6 +70,40 @@ open class AuthenticationService(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun authenticateUserWithBiometricToSetAsNewAuthenticationMethod(authenticationResult: (AuthenticationResult) -> Unit) {
|
||||||
|
val cipher = cryptographyManager.getInitializedCipherForEncryption(EncryptionKeyName)
|
||||||
|
|
||||||
|
biometricAuthenticationService.authenticate(cipher) { result ->
|
||||||
|
if (result.successful) {
|
||||||
|
this.encryptionCipherForBiometric = cipher
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationResult(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun authenticateUserWithBiometric(result: (Boolean) -> Unit) {
|
||||||
|
loadAuthenticationSettings()?.let { settings ->
|
||||||
|
val iv = decodeFromBase64(settings.initializationVector ?: "")
|
||||||
|
|
||||||
|
val cipher = cryptographyManager.getInitializedCipherForDecryption(EncryptionKeyName, iv)
|
||||||
|
biometricAuthenticationService.authenticate(cipher) { authenticationResult ->
|
||||||
|
if (authenticationResult.successful) {
|
||||||
|
settings.encryptedUserPassword?.let {
|
||||||
|
val encryptedUserPassword = decodeFromBase64(it)
|
||||||
|
val decrypted = cryptographyManager.decryptData(encryptedUserPassword, cipher)
|
||||||
|
|
||||||
|
result(openDatabase(decrypted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: run { result(false) }
|
||||||
|
}
|
||||||
|
|
||||||
protected open fun openDatabase(authenticationSettings: AuthenticationSettings) {
|
protected open fun openDatabase(authenticationSettings: AuthenticationSettings) {
|
||||||
openDatabase(authenticationSettings.userPassword)
|
openDatabase(authenticationSettings.userPassword)
|
||||||
}
|
}
|
||||||
|
@ -88,17 +130,26 @@ open class AuthenticationService(
|
||||||
val settings = loadOrCreateDefaultAuthenticationSettings()
|
val settings = loadOrCreateDefaultAuthenticationSettings()
|
||||||
|
|
||||||
if (type == settings.type &&
|
if (type == settings.type &&
|
||||||
((type != AuthenticationType.Password && settings.userPassword == newPassword)
|
((type == AuthenticationType.Password && isCorrectUserPassword(newPassword)) || settings.userPassword == newPassword)) { // nothing changed
|
||||||
|| (type == AuthenticationType.Password && isCorrectUserPassword(newPassword)))) { // nothing changed
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.type = type
|
settings.type = type
|
||||||
settings.hashedUserPassword = if (type == AuthenticationType.Password) BCrypt.withDefaults().hashToString(12, newPassword.toCharArray()) else null
|
settings.hashedUserPassword = if (type == AuthenticationType.Password) BCrypt.withDefaults().hashToString(12, newPassword.toCharArray()) else null
|
||||||
settings.userPassword = if (type == AuthenticationType.Password) null else newPassword
|
settings.userPassword = if (type == AuthenticationType.None) newPassword else null
|
||||||
|
|
||||||
|
if (type == AuthenticationType.Biometric) {
|
||||||
|
encryptionCipherForBiometric?.let { encryptionCipher ->
|
||||||
|
val encryptedPassword = cryptographyManager.encryptData(newPassword, encryptionCipher)
|
||||||
|
settings.encryptedUserPassword = encodeToBase64(encryptedPassword)
|
||||||
|
settings.initializationVector = encodeToBase64(encryptionCipher.iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (saveAuthenticationSettings(settings)) {
|
if (saveAuthenticationSettings(settings)) {
|
||||||
this.authenticationType = type
|
this.authenticationType = type
|
||||||
|
this.encryptionCipherForBiometric = null
|
||||||
|
|
||||||
persistence.changePassword(newPassword) // TODO: actually this is bad. If changing password fails then password is saved in AuthenticationSettings but DB has a different password
|
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 true
|
||||||
}
|
}
|
||||||
|
@ -158,4 +209,13 @@ open class AuthenticationService(
|
||||||
return passwordBuilder.toString()
|
return passwordBuilder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun encodeToBase64(data: ByteArray): String {
|
||||||
|
return Base64.encodeToString(data, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun decodeFromBase64(data: String): ByteArray {
|
||||||
|
return Base64.decode(data, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,7 +4,9 @@ package net.dankito.banking.ui.android.authentication
|
||||||
open class AuthenticationSettings(
|
open class AuthenticationSettings(
|
||||||
open var type: AuthenticationType,
|
open var type: AuthenticationType,
|
||||||
open var hashedUserPassword: String? = null,
|
open var hashedUserPassword: String? = null,
|
||||||
open var userPassword: String? = null
|
open var userPassword: String? = null,
|
||||||
|
open var encryptedUserPassword: String? = null,
|
||||||
|
open var initializationVector: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
internal constructor() : this(AuthenticationType.None) // for object deserializers
|
internal constructor() : this(AuthenticationType.None) // for object deserializers
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.biometric.BiometricPrompt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import net.dankito.banking.ui.android.R
|
import net.dankito.banking.ui.android.R
|
||||||
import net.dankito.banking.ui.android.util.CurrentActivityTracker
|
import net.dankito.banking.ui.android.util.CurrentActivityTracker
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
|
||||||
open class BiometricAuthenticationService(
|
open class BiometricAuthenticationService(
|
||||||
|
@ -18,7 +19,7 @@ open class BiometricAuthenticationService(
|
||||||
get() = biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
get() = biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
|
||||||
|
|
||||||
override fun authenticate(authenticationResult: (AuthenticationResult) -> Unit) {
|
override fun authenticate(cipher: Cipher?, authenticationResult: (AuthenticationResult) -> Unit) {
|
||||||
activityTracker.currentOrNextActivity { activity ->
|
activityTracker.currentOrNextActivity { activity ->
|
||||||
val executor = ContextCompat.getMainExecutor(context)
|
val executor = ContextCompat.getMainExecutor(context)
|
||||||
|
|
||||||
|
@ -44,11 +45,16 @@ open class BiometricAuthenticationService(
|
||||||
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||||
.setTitle(context.getString(R.string.activity_login_authenticate_with_biometrics_prompt))
|
.setTitle(context.getString(R.string.activity_login_authenticate_with_biometrics_prompt))
|
||||||
.setDeviceCredentialAllowed(true)
|
.setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
if (cipher == null) {
|
||||||
biometricPrompt.authenticate(promptInfo)
|
biometricPrompt.authenticate(promptInfo)
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
package net.dankito.banking.ui.android.authentication
|
package net.dankito.banking.ui.android.authentication
|
||||||
|
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
|
||||||
interface IBiometricAuthenticationService {
|
interface IBiometricAuthenticationService {
|
||||||
|
|
||||||
val supportsBiometricAuthentication: Boolean
|
val supportsBiometricAuthentication: Boolean
|
||||||
|
|
||||||
|
|
||||||
fun authenticate(authenticationResult: (AuthenticationResult) -> Unit)
|
fun authenticate(cipher: Cipher? = null, authenticationResult: (AuthenticationResult) -> Unit)
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package net.dankito.banking.ui.android.security
|
||||||
|
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
|
||||||
|
|
||||||
|
open class CryptographyManager {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
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
|
||||||
|
private const val EncryptionAlgorithm = KeyProperties.KEY_ALGORITHM_AES
|
||||||
|
|
||||||
|
private const val CipherTransformation = "$EncryptionAlgorithm/$EncryptionBlockMode/$EncryptionPadding"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun getInitializedCipherForEncryption(keyName: String): Cipher {
|
||||||
|
return getInitializedCipher(keyName, Cipher.ENCRYPT_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getInitializedCipherForDecryption(keyName: String, initializationVector: ByteArray): Cipher {
|
||||||
|
return getInitializedCipher(keyName, Cipher.DECRYPT_MODE, initializationVector)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getInitializedCipher(keyName: String, cipherMode: Int, initializationVector: ByteArray? = null): Cipher {
|
||||||
|
val cipher = Cipher.getInstance(CipherTransformation)
|
||||||
|
val secretKey = getOrCreateSecretKey(keyName)
|
||||||
|
|
||||||
|
cipher.init(cipherMode, secretKey, initializationVector?.let { GCMParameterSpec(128, initializationVector) })
|
||||||
|
|
||||||
|
return cipher
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun encryptData(plaintext: String, cipher: Cipher): ByteArray {
|
||||||
|
return cipher.doFinal(plaintext.toByteArray(PasswordCharset))
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun decryptData(cipherText: ByteArray, cipher: Cipher): String {
|
||||||
|
val plaintext = cipher.doFinal(cipherText)
|
||||||
|
return String(plaintext, PasswordCharset)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun getOrCreateSecretKey(keyName: String): SecretKey {
|
||||||
|
val keyStore = KeyStore.getInstance(AndroidKeyStore)
|
||||||
|
keyStore.load(null)
|
||||||
|
keyStore.getKey(keyName, null)?.let { return it as SecretKey }
|
||||||
|
|
||||||
|
val paramsBuilder = KeyGenParameterSpec.Builder(keyName,
|
||||||
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
|
paramsBuilder.apply {
|
||||||
|
setBlockModes(EncryptionBlockMode)
|
||||||
|
setEncryptionPaddings(EncryptionPadding)
|
||||||
|
setKeySize(KeySize)
|
||||||
|
setUserAuthenticationRequired(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyGenParams = paramsBuilder.build()
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
|
||||||
|
AndroidKeyStore)
|
||||||
|
keyGenerator.init(keyGenParams)
|
||||||
|
return keyGenerator.generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ open class BiometricAuthenticationButton @JvmOverloads constructor(
|
||||||
|
|
||||||
open var authenticationSuccessful: (() -> Unit)? = null
|
open var authenticationSuccessful: (() -> Unit)? = null
|
||||||
|
|
||||||
|
open var customButtonClickHandler: (() -> Unit)? = null
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
BankingComponent.component.inject(this)
|
BankingComponent.component.inject(this)
|
||||||
|
@ -33,7 +35,9 @@ open class BiometricAuthenticationButton @JvmOverloads constructor(
|
||||||
val rootView = inflater.inflate(R.layout.view_biometric_authentication_button, this, true)
|
val rootView = inflater.inflate(R.layout.view_biometric_authentication_button, this, true)
|
||||||
|
|
||||||
rootView.apply {
|
rootView.apply {
|
||||||
btnStartBiometricAuthentication.setOnClickListener { doBiometricAuthenticationAndLogIn() }
|
btnStartBiometricAuthentication.setOnClickListener {
|
||||||
|
customButtonClickHandler?.invoke() ?: doBiometricAuthenticationAndLogIn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +51,7 @@ open class BiometricAuthenticationButton @JvmOverloads constructor(
|
||||||
|
|
||||||
|
|
||||||
open fun showBiometricPrompt() {
|
open fun showBiometricPrompt() {
|
||||||
doBiometricAuthenticationAndLogIn()
|
btnStartBiometricAuthentication.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
android:layout_width="@dimen/view_biometric_authentication_button_icon_size"
|
android:layout_width="@dimen/view_biometric_authentication_button_icon_size"
|
||||||
android:layout_height="@dimen/view_biometric_authentication_button_icon_size"
|
android:layout_height="@dimen/view_biometric_authentication_button_icon_size"
|
||||||
android:padding="@dimen/view_biometric_authentication_button_padding"
|
android:padding="@dimen/view_biometric_authentication_button_padding"
|
||||||
android:background="@color/colorAccent"
|
android:background="@drawable/conditionally_disabled_view_background"
|
||||||
android:tint="#FFFFFF"
|
android:tint="#FFFFFF"
|
||||||
app:srcCompat="@drawable/ic_baseline_fingerprint_24"
|
app:srcCompat="@drawable/ic_baseline_fingerprint_24"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
|
|
|
@ -17,15 +17,28 @@ open class JacksonJsonSerializer(
|
||||||
}
|
}
|
||||||
) : ISerializer {
|
) : ISerializer {
|
||||||
|
|
||||||
|
override fun serializeObjectToString(obj: Any): String? {
|
||||||
|
return serializer.serializeObject(obj)
|
||||||
|
}
|
||||||
|
|
||||||
override fun serializeObject(obj: Any, outputFile: File) {
|
override fun serializeObject(obj: Any, outputFile: File) {
|
||||||
return serializer.serializeObject(obj, outputFile)
|
return serializer.serializeObject(obj, outputFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Any> deserializeObject(serializedObject: String, objectClass: KClass<T>, vararg genericParameterTypes: KClass<*>): T? {
|
||||||
|
return serializer.deserializeObject(serializedObject, objectClass.java, *genericParameterTypes.map { it.java }.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
override fun <T : Any> deserializeObject(serializedObjectFile: File, objectClass: KClass<T>,
|
override fun <T : Any> deserializeObject(serializedObjectFile: File, objectClass: KClass<T>,
|
||||||
vararg genericParameterTypes: KClass<*>): T? {
|
vararg genericParameterTypes: KClass<*>): T? {
|
||||||
return serializer.deserializeObject(serializedObjectFile, objectClass.java, *genericParameterTypes.map { it.java }.toTypedArray())
|
return serializer.deserializeObject(serializedObjectFile, objectClass.java, *genericParameterTypes.map { it.java }.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> deserializeListOr(serializedObject: String, genericListParameterType: KClass<T>, defaultValue: List<T>): List<T> {
|
||||||
|
return serializer.deserializeListOr(serializedObject, genericListParameterType.java, defaultValue) ?: defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
override fun <T : Any> deserializeListOr(serializedObjectFile: File, genericListParameterType: KClass<T>, defaultValue: List<T>): List<T> {
|
override fun <T : Any> deserializeListOr(serializedObjectFile: File, genericListParameterType: KClass<T>, defaultValue: List<T>): List<T> {
|
||||||
return serializer.deserializeListOr(serializedObjectFile, genericListParameterType.java, defaultValue)
|
return serializer.deserializeListOr(serializedObjectFile, genericListParameterType.java, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,13 @@ import kotlin.reflect.jvm.jvmName
|
||||||
open class JacksonClassNameIdResolver : ClassNameIdResolver(SimpleType.construct(JobParameters::class.java), TypeFactory.defaultInstance()) {
|
open class JacksonClassNameIdResolver : ClassNameIdResolver(SimpleType.construct(JobParameters::class.java), TypeFactory.defaultInstance()) {
|
||||||
|
|
||||||
override fun idFromValue(value: Any?): String {
|
override fun idFromValue(value: Any?): String {
|
||||||
|
println("value class is ${value?.javaClass?.simpleName}")
|
||||||
|
|
||||||
if (value is RetrieveAccountTransactionsParameters) {
|
if (value is RetrieveAccountTransactionsParameters) {
|
||||||
return RetrieveAccountTransactionsParameters::class.jvmName
|
return RetrieveAccountTransactionsParameters::class.jvmName
|
||||||
}
|
}
|
||||||
else if (value is SepaAccountInfoParameters) {
|
else if (value is SepaAccountInfoParameters) {
|
||||||
|
println("Returning SepaAccountInfoParameters")
|
||||||
return SepaAccountInfoParameters::class.jvmName
|
return SepaAccountInfoParameters::class.jvmName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue