package com.devicebinding import android.content.Context import android.content.SharedPreferences import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.security.keystore.StrongBoxUnavailableException import com.facebook.react.bridge.* import java.security.* import java.security.spec.ECGenParameterSpec import java.util.UUID class DeviceBindingModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName(): String = "DeviceBinding" private val keyStore: KeyStore by lazy { KeyStore.getInstance("AndroidKeyStore").apply { load(null) } } private fun getPrefs(): SharedPreferences { return reactApplicationContext.getSharedPreferences("DeviceBindingPrefs", Context.MODE_PRIVATE) } private fun aliasName(alias: String): String = "device_binding_$alias" @ReactMethod fun generateKeyPair(alias: String, promise: Promise) { try { val keyAlias = aliasName(alias) if (keyStore.containsAlias(keyAlias)) { keyStore.deleteEntry(keyAlias) } val keyPairGenerator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore" ) var strongBoxUsed = false var keyPair: KeyPair? = null // Try with StrongBox first on API 28+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { try { val spec = KeyGenParameterSpec.Builder( keyAlias, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY ) .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) .setDigests(KeyProperties.DIGEST_SHA256) .setUserAuthenticationRequired(false) .setIsStrongBoxBacked(true) .build() keyPairGenerator.initialize(spec) keyPair = keyPairGenerator.generateKeyPair() strongBoxUsed = true } catch (_: Exception) { // StrongBox failed, clean up and fall through to software keystore if (keyStore.containsAlias(keyAlias)) { keyStore.deleteEntry(keyAlias) } } } // Fallback without StrongBox if (keyPair == null) { val spec = KeyGenParameterSpec.Builder( keyAlias, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY ) .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) .setDigests(KeyProperties.DIGEST_SHA256) .setUserAuthenticationRequired(false) .build() keyPairGenerator.initialize(spec) keyPair = keyPairGenerator.generateKeyPair() } val publicKeyBytes = keyPair.public.encoded val publicKeyBase64 = android.util.Base64.encodeToString( publicKeyBytes, android.util.Base64.NO_WRAP ) val attestation = if (strongBoxUsed) { "android_keystore_strongbox" } else { "android_keystore" } val result = Arguments.createMap() result.putString("publicKey", publicKeyBase64) result.putString("keyAttestation", attestation) promise.resolve(result) } catch (e: Exception) { promise.reject("KEY_GEN_ERROR", "Failed to generate key pair: ${e.message}", e) } } @ReactMethod fun getPublicKey(alias: String, promise: Promise) { try { val keyAlias = aliasName(alias) val entry = keyStore.getEntry(keyAlias, null) if (entry !is KeyStore.PrivateKeyEntry) { promise.reject("KEY_NOT_FOUND", "No key pair found for alias: $alias") return } val publicKeyBytes = entry.certificate.publicKey.encoded val publicKeyBase64 = android.util.Base64.encodeToString( publicKeyBytes, android.util.Base64.NO_WRAP ) promise.resolve(publicKeyBase64) } catch (e: Exception) { promise.reject("KEY_ERROR", "Failed to get public key: ${e.message}", e) } } @ReactMethod fun signChallenge(alias: String, challenge: String, promise: Promise) { try { val keyAlias = aliasName(alias) val entry = keyStore.getEntry(keyAlias, null) if (entry !is KeyStore.PrivateKeyEntry) { promise.reject("KEY_NOT_FOUND", "No key pair found for alias: $alias") return } val signature = Signature.getInstance("SHA256withECDSA") signature.initSign(entry.privateKey) signature.update(challenge.toByteArray(Charsets.UTF_8)) val signatureBytes = signature.sign() val signatureBase64 = android.util.Base64.encodeToString( signatureBytes, android.util.Base64.NO_WRAP ) promise.resolve(signatureBase64) } catch (e: Exception) { promise.reject("SIGN_ERROR", "Failed to sign challenge: ${e.message}", e) } } @ReactMethod fun deleteKeyPair(alias: String, promise: Promise) { try { val keyAlias = aliasName(alias) if (keyStore.containsAlias(keyAlias)) { keyStore.deleteEntry(keyAlias) promise.resolve(true) } else { promise.resolve(false) } } catch (e: Exception) { promise.reject("DELETE_ERROR", "Failed to delete key pair: ${e.message}", e) } } @ReactMethod fun hasKeyPair(alias: String, promise: Promise) { try { val keyAlias = aliasName(alias) promise.resolve(keyStore.containsAlias(keyAlias)) } catch (e: Exception) { promise.reject("CHECK_ERROR", "Failed to check key pair: ${e.message}", e) } } @ReactMethod fun isStrongBoxAvailable(promise: Promise) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { promise.resolve( reactApplicationContext.packageManager.hasSystemFeature( "android.hardware.strongbox_keystore" ) ) } else { promise.resolve(false) } } @ReactMethod fun getDeviceId(promise: Promise) { val prefs = getPrefs() var deviceId = prefs.getString("deviceId", null) if (deviceId == null) { deviceId = UUID.randomUUID().toString() prefs.edit().putString("deviceId", deviceId).apply() } promise.resolve(deviceId) } @ReactMethod fun resetDeviceId(promise: Promise) { val newId = UUID.randomUUID().toString() getPrefs().edit().putString("deviceId", newId).apply() promise.resolve(newId) } }