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