Я пытаюсь получить одни и те же ключи каждый раз, когда запускаю свое приложение — это делается для того, чтобы я мог легко отлаживать и получать одинаковые выходные данные для своих функций при каждом запуске.
Однако у меня возникла очень странная проблема: в моих модульных тестах это работает так, как ожидалось, и каждый раз возвращает один и тот же открытый ключ. Однако если я запускаю один и тот же код из своего основного приложения, оно каждый раз генерирует другой код.
Создать терминалEphemeralPublicPrivateKeys
package com.test.tomtestplayground
import org.bouncycastle.asn1.x9.ECNamedCurveTable
import java.math.BigInteger
import java.security.InvalidAlgorithmParameterException
import java.security.KeyPairGenerator
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.security.interfaces.ECPublicKey
import java.security.spec.ECGenParameterSpec
class GenerateTerminalEphemeralPublicPrivateKeys {
@OptIn(ExperimentalStdlibApi::class)
@Throws(NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class)
fun generate(): String {
// Fixed seed for SecureRandom to generate the same key pair every time
val secureRandom = SecureRandom.getInstance("SHA1PRNG")
secureRandom.setSeed("test-seed".toByteArray())
val keyGen = KeyPairGenerator.getInstance("EC")
keyGen.initialize(ECGenParameterSpec("secp256r1"), secureRandom)
val pair = keyGen.generateKeyPair()
val publicKey = pair.public as ECPublicKey
val privateKey = pair.private
val x: ByteArray = publicKey.getW().getAffineX().toByteArray()
val y: ByteArray = publicKey.getW().getAffineY().toByteArray()
val xbi = BigInteger(1, x)
val ybi = BigInteger(1, y)
val x9 = ECNamedCurveTable.getByName("secp256r1")
val curve = x9.curve
val point = curve.createPoint(xbi, ybi)
val publicKeyByteArray = point.getEncoded(true)
return publicKeyByteArray.toHexString()
}
}
Создать терминалEphemeralPublicPrivateKeysTest
Это работает так, как ожидалось, и проходит каждый раз
package com.test.tomtestplayground
import android.nfc.Tag
import android.nfc.tech.IsoDep
import io.mockk.*
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.security.Security
@RunWith(RobolectricTestRunner::class)
class GenerateTerminalEphemeralPublicPrivateKeysTest {
private val generateTerminalEphemeralPublicPrivateKeys = GenerateTerminalEphemeralPublicPrivateKeys()
@Before
fun setUp() {
Security.addProvider(BouncyCastleProvider())
}
@Test
fun shouldReturnSameKeyEveryTime() {
val publicKeyCompressed = generateTerminalEphemeralPublicPrivateKeys.generate()
assertEquals(publicKeyCompressed, "032f0ca7da7eb706c7de924e534bcb76e36a471ed2ea0c99b2ad59e70394d83bf6")
}
}
Основная деятельность
При этом каждый раз регистрируется другой ключ, что неверно.
package com.test.tomtestplayground
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.passentry.tomtestplayground.ui.theme.TomTestPlaygroundTheme
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Security.addProvider(BouncyCastleProvider())
Log.d("MainActivity", GenerateTerminalEphemeralPublicPrivateKeys().generate())
setContent {
TomTestPlaygroundTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
TomTestPlaygroundTheme {
Greeting("Android")
}
}
Сборка.gradle
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
}
android {
namespace = "com.passentry.tomtestplayground"
compileSdk = 34
defaultConfig {
applicationId = "com.passentry.tomtestplayground"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation("org.bouncycastle:bcpkix-jdk15on:1.68")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
testImplementation("org.robolectric:robolectric:4.11.1")
testImplementation("org.bouncycastle:bcprov-jdk15on:1.68") // Added BouncyCastle provider
testImplementation("org.bouncycastle:bcpkix-jdk15on:1.68") // Added BouncyCastle PKIX
testImplementation("io.mockk:mockk:1.12.0")
}
Или примените специальную функцию получения ключей, например PBKDF2, вместо генератора случайных чисел. Ключевая функция вывода гарантирует воспроизводимость.
@Topaco У меня та же проблема с PBKDF2. Каждый раз, когда я запускаю действие, он генерирует другой, но в тестах он один и тот же.
@ElliottFrisch, спасибо, но я не знаю, как это сделать. KeyGen требует SecureRandom
Тогда вы делаете что-то не так. Без кода невозможно сказать, что именно. Опубликуйте MCVE, который содержит только соответствующую логику.
Спасибо @Topaco, теперь это работает.
import org.bouncycastle.crypto.params.ECDomainParameters
import org.bouncycastle.crypto.params.ECPrivateKeyParameters
import org.bouncycastle.crypto.params.ECPublicKeyParameters
import org.bouncycastle.math.ec.FixedPointCombMultiplier
import java.math.BigInteger
import java.security.InvalidAlgorithmParameterException
import java.security.NoSuchAlgorithmException
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
class GenerateTerminalEphemeralPublicPrivateKeys {
@OptIn(ExperimentalStdlibApi::class)
@Throws(NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class)
fun generate(): String {
// Derive key from a fixed seed using PBKDF2
val seed = "test-seed"
val salt = ByteArray(16) { 0 } // fixed salt for deterministic output
val iterations = 10000
val keyLength = 256
val keySpec = PBEKeySpec(seed.toCharArray(), salt, iterations, keyLength)
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val derivedKey = secretKeyFactory.generateSecret(keySpec).encoded
// Convert derived key to BigInteger
val privateKeyD = BigInteger(1, derivedKey.sliceArray(0 until 32))
// Use the derived private key to generate the public key
val x9 = CustomNamedCurves.getByName("secp256r1")
val curve = x9.curve
val domainParams = ECDomainParameters(curve, x9.g, x9.n, x9.h)
val privateKeyParams = ECPrivateKeyParameters(privateKeyD, domainParams)
val publicKeyParams = ECPublicKeyParameters(
FixedPointCombMultiplier().multiply(domainParams.g, privateKeyD),
domainParams
)
val publicKeyByteArray = publicKeyParams.q.getEncoded(true)
return publicKeyByteArray.toHexString()
}
private fun ByteArray.toHexString(): String {
return joinToString("") { "%02x".format(it) }
}
}
SecureRandom
не полагается исключительно на начальное значение, предоставленное пользователем. Он будет генерировать новые случайные значения независимо от того, какое начальное значение вы предоставите. Если вам нужна повторяемость, используйте обычныйRandom
.