javaandroidkotlinsecuritycryptography

Secure Random Seed Not Working In MainActivity


I am trying to get the same keys every time I run my app - this is so I can easily debug and have the same output for my functions every time I run.

I am getting a super weird problem though, where in my unit tests, this works as expected and returns the same public key every time. However, if I run the exact same code from my main application, it generates a different code every time.

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()
    }
}

GenerateTerminalEphemeralPublicPrivateKeysTest

This works as expected and passes every time

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

This logs a different key every single time - which is wrong.

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

}

Solution

  • Thanks @Topaco, this works now.

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