Android中MainActivity固定Secure Random种子生成密钥失效问题
问题:固定种子生成密钥,单元测试一致但MainActivity每次不同
我希望每次运行应用时生成相同的密钥,方便调试时得到一致的输出。但现在遇到问题:单元测试里代码每次返回相同公钥,可在MainActivity中运行相同代码,每次生成的密钥都不一样。
GenerateTerminalEphemeralPublicPrivateKeys 类
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() } }
单元测试代码(运行正常,每次返回相同密钥)
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") } }
MainActivity代码(每次运行输出不同密钥)
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") } }
Build.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") }
解决方案
问题根源
SHA1PRNG实现差异:Android系统和Robolectric测试环境中,SHA1PRNG的底层实现不同。Android的SecureRandom默认会先混入系统随机源,即使调用setSeed也无法完全重置状态,导致生成的随机序列不一致。- Provider优先级问题:测试环境中明确使用了BouncyCastle作为Security Provider,但在MainActivity中,
KeyPairGenerator.getInstance("EC")可能优先使用了Android系统自带的EC实现,而非BouncyCastle的实现,两者的随机数处理逻辑不同。
修复步骤
- 指定使用BouncyCastle的KeyPairGenerator
在获取KeyPairGenerator时,明确指定BouncyCastle作为Provider:val keyGen = KeyPairGenerator.getInstance("EC", "BC") - 使用BouncyCastle的SecureRandom实现
直接获取BouncyCastle提供的SecureRandom,避免系统实现的差异:val secureRandom = SecureRandom.getInstance("SHA1PRNG", "BC") - 重置SecureRandom状态
调用setSeed前,先调用secureRandom.setSeed(byteArrayOf())清空原有状态,确保种子完全可控:secureRandom.setSeed(byteArrayOf()) secureRandom.setSeed("test-seed".toByteArray())
修改后的generate方法
@OptIn(ExperimentalStdlibApi::class) @Throws(NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class) fun generate(): String { // 使用BouncyCastle的SecureRandom实现 val secureRandom = SecureRandom.getInstance("SHA1PRNG", "BC") // 清空原有状态,确保种子生效 secureRandom.setSeed(byteArrayOf()) secureRandom.setSeed("test-seed".toByteArray()) // 明确指定BouncyCastle作为EC密钥生成的Provider val keyGen = KeyPairGenerator.getInstance("EC", "BC") keyGen.initialize(ECGenParameterSpec("secp256r1"), secureRandom) val pair = keyGen.generateKeyPair() val publicKey = pair.public as ECPublicKey val xbi = publicKey.w.affineX val ybi = publicKey.w.affineY val x9 = ECNamedCurveTable.getByName("secp256r1") val curve = x9.curve val point = curve.createPoint(xbi, ybi) val publicKeyByteArray = point.getEncoded(true) return publicKeyByteArray.toHexString() }
额外注意事项
- 确保BouncyCastle Provider在应用启动时被正确添加,且优先级足够高(可以使用
Security.insertProviderAt(BouncyCastleProvider(), 1)替代addProvider,确保优先使用BC)。 - 仅在调试环境中使用固定种子,正式发布版本必须使用不可预测的随机种子,避免安全风险。
内容的提问来源于stack exchange,提问作者Tom Rowbotham
相关产品推荐
相关产品推荐

