You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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")

}

解决方案

问题根源

  1. SHA1PRNG实现差异:Android系统和Robolectric测试环境中,SHA1PRNG的底层实现不同。Android的SecureRandom默认会先混入系统随机源,即使调用setSeed也无法完全重置状态,导致生成的随机序列不一致。
  2. Provider优先级问题:测试环境中明确使用了BouncyCastle作为Security Provider,但在MainActivity中,KeyPairGenerator.getInstance("EC")可能优先使用了Android系统自带的EC实现,而非BouncyCastle的实现,两者的随机数处理逻辑不同。

修复步骤

  1. 指定使用BouncyCastle的KeyPairGenerator
    在获取KeyPairGenerator时,明确指定BouncyCastle作为Provider:
    val keyGen = KeyPairGenerator.getInstance("EC", "BC")
    
  2. 使用BouncyCastle的SecureRandom实现
    直接获取BouncyCastle提供的SecureRandom,避免系统实现的差异:
    val secureRandom = SecureRandom.getInstance("SHA1PRNG", "BC")
    
  3. 重置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 02:24:52