Updated on 2026-08-14
This commit is contained in:
parent
f0f5fe96fc
commit
e3d2e7f2e1
15 changed files with 476 additions and 1 deletions
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -102,5 +102,9 @@
|
|||
{
|
||||
"name": "AND_15154_YIELD_PROMO_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
|
|
@ -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<Test>().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)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.lib.auth
|
||||
|
||||
interface AuthFeatureToggles {
|
||||
val isBackendAuthenticationEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<ByteArray>
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.lib.auth.devicekey
|
||||
|
||||
class DeviceKeySigningException(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ByteArray> = 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ByteArray> = None
|
||||
|
||||
override suspend fun sign(data: ByteArray): ByteArray =
|
||||
throw DeviceKeySigningException("DeviceKeyManager is disabled: backend authentication feature toggle is off")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<KeyPairGenerator>(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<java.security.PublicKey>()
|
||||
every { publicKey.encoded } returns encoded
|
||||
|
||||
val cert = mockk<Certificate>()
|
||||
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<java.security.PublicKey>()
|
||||
every { publicKey.encoded } returns encoded
|
||||
|
||||
val cert = mockk<Certificate>()
|
||||
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<PrivateKey>()
|
||||
every { keyStore.getKey(KEY_ALIAS, null) } returns privateKey
|
||||
|
||||
val javaSig = mockk<java.security.Signature>()
|
||||
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<DeviceKeySigningException> {
|
||||
manager.sign("data".toByteArray())
|
||||
}
|
||||
assertThat(exception.message).contains("Device key not found")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sign wraps unexpected exception in DeviceKeySigningException`() = runTest {
|
||||
val privateKey = mockk<PrivateKey>()
|
||||
every { keyStore.getKey(KEY_ALIAS, null) } returns privateKey
|
||||
|
||||
val javaSig = mockk<java.security.Signature>()
|
||||
every { javaSig.initSign(privateKey) } throws RuntimeException("hardware error")
|
||||
|
||||
mockkSignatureGetInstance(javaSig)
|
||||
|
||||
val exception = assertThrows<DeviceKeySigningException> {
|
||||
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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue