diff --git a/.mcp.json b/.mcp.json index 009df155b7..3721a9071a 100644 --- a/.mcp.json +++ b/.mcp.json @@ -9,6 +9,11 @@ "type": "stdio", "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"] + }, + "notion": { + "type": "stdio", + "command": "npx", + "args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"] } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 4c7aa189cb..88b77270c2 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -5,6 +5,7 @@ import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.analytics.paramsinterceptor.SendTransactionSignerInfoInterceptor import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager +import com.tangem.lib.auth.devicekey.DeviceKeyManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.local.config.environment.EnvironmentConfig @@ -49,4 +50,6 @@ interface ApplicationEntryPoint { fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory fun getSendTransactionSignerInfoInterceptor(): SendTransactionSignerInfoInterceptor + + fun getDeviceKeyManager(): DeviceKeyManager } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 3684c3af20..cca253b4cc 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -21,6 +21,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.common.LogConfig import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.lib.auth.devicekey.DeviceKeyManager import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler @@ -92,6 +93,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val sendTransactionSignerInfoInterceptor get() = entryPoint.getSendTransactionSignerInfoInterceptor() + private val deviceKeyManager: DeviceKeyManager + get() = entryPoint.getDeviceKeyManager() + // endregion private val appScope = MainScope() @@ -132,6 +136,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. } fun init() { + appScope.launch { + deviceKeyManager.generateIfMissing() + } walletsRepository = entryPoint.getWalletsRepository() apiConfigsManager.initialize() diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 9d19ee9e70..f598f5ba1e 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -102,5 +102,9 @@ { "name": "AND_15154_YIELD_PROMO_ENABLED", "version": "undefined" + }, + { + "name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED", + "version": "undefined" } ] diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1b0d2dc074..b745566b76 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1527" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-614" +tangemCardSdk = "develop-620" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/auth/build.gradle.kts b/libs/auth/build.gradle.kts index e1341f668a..a837781837 100644 --- a/libs/auth/build.gradle.kts +++ b/libs/auth/build.gradle.kts @@ -1,9 +1,42 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } android { namespace = "com.tangem.lib.auth" +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + /** Core */ + implementation(projects.core.utils) + implementation(projects.core.configToggles) + + /** Tangem libraries */ + implementation(tangemDeps.card.core) + + /** Firebase */ + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.crashlytics) + + /** Other */ + implementation(deps.arrow.core) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** Tests */ + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/AuthFeatureToggles.kt b/libs/auth/src/main/java/com/tangem/lib/auth/AuthFeatureToggles.kt new file mode 100644 index 0000000000..a2eb2b378b --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/AuthFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.lib.auth + +interface AuthFeatureToggles { + val isBackendAuthenticationEnabled: Boolean +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/DefaultAuthFeatureToggles.kt b/libs/auth/src/main/java/com/tangem/lib/auth/DefaultAuthFeatureToggles.kt new file mode 100644 index 0000000000..a7151aec10 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/DefaultAuthFeatureToggles.kt @@ -0,0 +1,13 @@ +package com.tangem.lib.auth + +import com.tangem.core.configtoggle.FeatureToggles +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import javax.inject.Inject + +internal class DefaultAuthFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : AuthFeatureToggles { + + override val isBackendAuthenticationEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15438_BACKEND_AUTHENTICATION_ENABLED) +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/DeviceKeyManager.kt b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/DeviceKeyManager.kt new file mode 100644 index 0000000000..23e34014bb --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/DeviceKeyManager.kt @@ -0,0 +1,27 @@ +package com.tangem.lib.auth.devicekey + +import arrow.core.Option + +/** + * Manages a device-bound secp256r1 keypair in Android Keystore (TEE/StrongBox). + * The private key never leaves the secure hardware. + */ +interface DeviceKeyManager { + + /** + * Ensures the device keypair exists. Generates one if missing. + * Never throws — generation failures are logged and reported via the return value. + * @return `true` if a new keypair was generated, `false` if it already existed or generation failed + */ + suspend fun generateIfMissing(): Boolean + + /** Raw uncompressed public key (0x04 || x || y), or [arrow.core.None] if it cannot be read. */ + suspend fun getPublicKey(): Option + + /** + * Signs [data] with SHA256withECDSA using the device private key. + * @return raw 64-byte signature (r || s), each component zero-padded to 32 bytes + * @throws DeviceKeySigningException if signing fails + */ + suspend fun sign(data: ByteArray): ByteArray +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/DeviceKeySigningException.kt b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/DeviceKeySigningException.kt new file mode 100644 index 0000000000..94575b72c0 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/DeviceKeySigningException.kt @@ -0,0 +1,3 @@ +package com.tangem.lib.auth.devicekey + +class DeviceKeySigningException(message: String, cause: Throwable? = null) : Exception(message, cause) \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/di/DeviceKeyModule.kt b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/di/DeviceKeyModule.kt new file mode 100644 index 0000000000..24b93f68da --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/di/DeviceKeyModule.kt @@ -0,0 +1,38 @@ +package com.tangem.lib.auth.devicekey.di + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.lib.auth.AuthFeatureToggles +import com.tangem.lib.auth.devicekey.DeviceKeyManager +import com.tangem.lib.auth.devicekey.internal.DefaultDeviceKeyManager +import com.tangem.lib.auth.devicekey.internal.DisabledDeviceKeyManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.security.KeyStore +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object DeviceKeyModule { + + @Provides + @Singleton + fun provideDeviceKeyManager( + authFeatureToggles: AuthFeatureToggles, + dispatchers: CoroutineDispatcherProvider, + ): DeviceKeyManager { + if (!authFeatureToggles.isBackendAuthenticationEnabled) return DisabledDeviceKeyManager + + return runCatching { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + DefaultDeviceKeyManager(keyStore, dispatchers) + }.getOrElse { e -> + TangemLogger.e("Failed to init AndroidKeyStore, falling back to disabled DeviceKeyManager", e) + FirebaseCrashlytics.getInstance().recordException(e) + DisabledDeviceKeyManager + } + } +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/internal/DefaultDeviceKeyManager.kt b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/internal/DefaultDeviceKeyManager.kt new file mode 100644 index 0000000000..79520eb33a --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/internal/DefaultDeviceKeyManager.kt @@ -0,0 +1,125 @@ +package com.tangem.lib.auth.devicekey.internal + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import arrow.core.None +import arrow.core.Option +import com.tangem.crypto.Secp256r1 +import com.tangem.lib.auth.devicekey.DeviceKeyManager +import com.tangem.lib.auth.devicekey.DeviceKeySigningException +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.withContext +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.Signature +import java.security.spec.ECGenParameterSpec + +internal class DefaultDeviceKeyManager( + private val keyStore: KeyStore, + private val dispatchers: CoroutineDispatcherProvider, +) : DeviceKeyManager { + + override suspend fun generateIfMissing(): Boolean = withContext(dispatchers.io) { + if (keyStore.containsAlias(KEY_ALIAS)) return@withContext false + + try { + generateKey() + TangemLogger.i("Device key generated") + true + } catch (e: Exception) { + TangemLogger.e("Failed to generate device key", e) + false + } + } + + override suspend fun getPublicKey(): Option = withContext(dispatchers.io) { + Option.catch( + recover = { e -> + TangemLogger.e("Failed to get device public key", e) + None + }, + f = ::getPublicKeyBytes, + ) + } + + override suspend fun sign(data: ByteArray): ByteArray = withContext(dispatchers.io) { + try { + val privateKey = keyStore.getKey(KEY_ALIAS, null) + ?: throw DeviceKeySigningException("Device key not found") + + val signature = Signature.getInstance(SIGNATURE_ALGORITHM).apply { + initSign(privateKey as java.security.PrivateKey) + update(data) + } + + val derSignature = signature.sign() + Secp256r1.toByte64(derSignature) + } catch (e: DeviceKeySigningException) { + TangemLogger.e("Device key signing failed", e) + throw e + } catch (e: Exception) { + TangemLogger.e("Device key signing failed", e) + throw DeviceKeySigningException("Signing failed", e) + } + } + + private fun generateKey() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + initAndGenerateKeyPair(strongBox = true) + } catch (e: Exception) { + TangemLogger.i("StrongBox unavailable, falling back to TEE", e) + initAndGenerateKeyPair(strongBox = false) + } + } else { + initAndGenerateKeyPair(strongBox = false) + } + } + + private fun initAndGenerateKeyPair(strongBox: Boolean) { + val spec = buildKeyGenSpec(strongBox) + val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_PROVIDER) + generator.initialize(spec) + generator.generateKeyPair() + } + + private fun buildKeyGenSpec(strongBox: Boolean): KeyGenParameterSpec { + val builder = KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY, + ) + .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setUserAuthenticationRequired(false) + + if (strongBox && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder.setIsStrongBoxBacked(true) + } + + return builder.build() + } + + private fun getPublicKeyBytes(): ByteArray { + val cert = checkNotNull(keyStore.getCertificate(KEY_ALIAS)) { "Device key not found" } + + val encoded = cert.publicKey.encoded + check(encoded.size >= EC_UNCOMPRESSED_POINT_SIZE) { + "Invalid encoded public key: expected at least $EC_UNCOMPRESSED_POINT_SIZE bytes, got ${encoded.size}" + } + val point = encoded.copyOfRange(encoded.size - EC_UNCOMPRESSED_POINT_SIZE, encoded.size) + check(point[0] == UNCOMPRESSED_POINT_PREFIX) { + "Invalid EC public key: expected uncompressed point prefix 0x04, got 0x${"%02x".format(point[0])}" + } + return point + } + + private companion object { + const val KEYSTORE_PROVIDER = "AndroidKeyStore" + const val KEY_ALIAS = "tangem_device_key" + const val SIGNATURE_ALGORITHM = "SHA256withECDSA" + const val EC_UNCOMPRESSED_POINT_SIZE = 65 + const val UNCOMPRESSED_POINT_PREFIX = 0x04.toByte() + } +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/internal/DisabledDeviceKeyManager.kt b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/internal/DisabledDeviceKeyManager.kt new file mode 100644 index 0000000000..6ba337519c --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/devicekey/internal/DisabledDeviceKeyManager.kt @@ -0,0 +1,16 @@ +package com.tangem.lib.auth.devicekey.internal + +import arrow.core.None +import arrow.core.Option +import com.tangem.lib.auth.devicekey.DeviceKeyManager +import com.tangem.lib.auth.devicekey.DeviceKeySigningException + +internal object DisabledDeviceKeyManager : DeviceKeyManager { + + override suspend fun generateIfMissing(): Boolean = false + + override suspend fun getPublicKey(): Option = None + + override suspend fun sign(data: ByteArray): ByteArray = + throw DeviceKeySigningException("DeviceKeyManager is disabled: backend authentication feature toggle is off") +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/di/AuthFeatureTogglesModule.kt b/libs/auth/src/main/java/com/tangem/lib/auth/di/AuthFeatureTogglesModule.kt new file mode 100644 index 0000000000..9a31739046 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/di/AuthFeatureTogglesModule.kt @@ -0,0 +1,21 @@ +package com.tangem.lib.auth.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.lib.auth.AuthFeatureToggles +import com.tangem.lib.auth.DefaultAuthFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AuthFeatureTogglesModule { + + @Provides + @Singleton + fun provideAuthFeatureToggles(featureTogglesManager: FeatureTogglesManager): AuthFeatureToggles { + return DefaultAuthFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/libs/auth/src/test/java/com/tangem/lib/auth/devicekey/internal/DefaultDeviceKeyManagerTest.kt b/libs/auth/src/test/java/com/tangem/lib/auth/devicekey/internal/DefaultDeviceKeyManagerTest.kt new file mode 100644 index 0000000000..8f7cf3187d --- /dev/null +++ b/libs/auth/src/test/java/com/tangem/lib/auth/devicekey/internal/DefaultDeviceKeyManagerTest.kt @@ -0,0 +1,175 @@ +package com.tangem.lib.auth.devicekey.internal + +import arrow.core.None +import com.google.common.truth.Truth.assertThat +import com.tangem.lib.auth.devicekey.DeviceKeySigningException +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.assertThrows +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.security.cert.Certificate + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DefaultDeviceKeyManagerTest { + + private val keyStore: KeyStore = mockk(relaxed = true) + private val dispatchers = TestingCoroutineDispatcherProvider() + private val manager: DefaultDeviceKeyManager = DefaultDeviceKeyManager(keyStore, dispatchers) + + @BeforeEach + fun setup() { + clearMocks(keyStore) + } + + @AfterEach + fun teardown() { + unmockkAll() + } + + @Test + fun `generateIfMissing returns false when key already exists`() = runTest { + every { keyStore.containsAlias(KEY_ALIAS) } returns true + + val result = manager.generateIfMissing() + + assertThat(result).isFalse() + } + + @Test + fun `generateIfMissing returns false when generation fails`() = runTest { + every { keyStore.containsAlias(KEY_ALIAS) } returns false + + val keyPairGenerator = mockk(relaxed = true) + every { keyPairGenerator.generateKeyPair() } throws RuntimeException("keystore unavailable") + + mockkStatic(KeyPairGenerator::class) + every { KeyPairGenerator.getInstance("EC", "AndroidKeyStore") } returns keyPairGenerator + + val result = manager.generateIfMissing() + + assertThat(result).isFalse() + } + + @Test + fun `getPublicKey returns last 65 bytes from encoded key`() = runTest { + val rawPoint = ByteArray(65) { (it + 1).toByte() }.apply { this[0] = 0x04 } + val x509Header = ByteArray(26) { 0x30 } + val encoded = x509Header + rawPoint + + val publicKey = mockk() + every { publicKey.encoded } returns encoded + + val cert = mockk() + every { cert.publicKey } returns publicKey + every { keyStore.getCertificate(KEY_ALIAS) } returns cert + + val result = manager.getPublicKey() + + assertThat(result.getOrNull()).isEqualTo(rawPoint) + } + + @Test + fun `getPublicKey returns None when certificate not found`() = runTest { + every { keyStore.getCertificate(KEY_ALIAS) } returns null + + val result = manager.getPublicKey() + + assertThat(result).isEqualTo(None) + } + + @Test + fun `getPublicKey returns None when point prefix is not uncompressed`() = runTest { + val rawPoint = ByteArray(65) { (it + 1).toByte() }.apply { this[0] = 0x02 } + val x509Header = ByteArray(26) { 0x30 } + val encoded = x509Header + rawPoint + + val publicKey = mockk() + every { publicKey.encoded } returns encoded + + val cert = mockk() + every { cert.publicKey } returns publicKey + every { keyStore.getCertificate(KEY_ALIAS) } returns cert + + val result = manager.getPublicKey() + + assertThat(result).isEqualTo(None) + } + + @Test + fun `sign returns raw 64-byte signature`() = runTest { + val data = "test data".toByteArray() + val r = ByteArray(32) { 0x01 } + val s = ByteArray(32) { 0x02 } + val derSignature = buildDer(r, s) + + val privateKey = mockk() + every { keyStore.getKey(KEY_ALIAS, null) } returns privateKey + + val javaSig = mockk() + every { javaSig.initSign(privateKey) } returns Unit + every { javaSig.update(data) } returns Unit + every { javaSig.sign() } returns derSignature + + mockkSignatureGetInstance(javaSig) + + val result = manager.sign(data) + + assertThat(result).hasLength(64) + assertThat(result.copyOfRange(0, 32)).isEqualTo(r) + assertThat(result.copyOfRange(32, 64)).isEqualTo(s) + } + + @Test + fun `sign throws DeviceKeySigningException when key not found`() = runTest { + every { keyStore.getKey(KEY_ALIAS, null) } returns null + + val exception = assertThrows { + manager.sign("data".toByteArray()) + } + assertThat(exception.message).contains("Device key not found") + } + + @Test + fun `sign wraps unexpected exception in DeviceKeySigningException`() = runTest { + val privateKey = mockk() + every { keyStore.getKey(KEY_ALIAS, null) } returns privateKey + + val javaSig = mockk() + every { javaSig.initSign(privateKey) } throws RuntimeException("hardware error") + + mockkSignatureGetInstance(javaSig) + + val exception = assertThrows { + manager.sign("data".toByteArray()) + } + assertThat(exception.message).isEqualTo("Signing failed") + assertThat(exception.cause).isInstanceOf(RuntimeException::class.java) + } + + private fun mockkSignatureGetInstance(mock: java.security.Signature) { + io.mockk.mockkStatic(java.security.Signature::class) + every { java.security.Signature.getInstance("SHA256withECDSA") } returns mock + } + + private fun buildDer(r: ByteArray, s: ByteArray): ByteArray { + val rTlv = byteArrayOf(0x02, r.size.toByte()) + r + val sTlv = byteArrayOf(0x02, s.size.toByte()) + s + val body = rTlv + sTlv + return byteArrayOf(0x30, body.size.toByte()) + body + } + + private companion object { + const val KEY_ALIAS = "tangem_device_key" + } +} \ No newline at end of file