Skip to content

Commit

Permalink
fix(rn): get login working e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
moldy530 committed Nov 6, 2024
1 parent b84f08e commit 5c58383
Show file tree
Hide file tree
Showing 10 changed files with 1,560 additions and 110 deletions.
1 change: 1 addition & 0 deletions account-kit/rn-signer/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ dependencies {
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "javax.xml.bind:jaxb-api:2.3.1"
implementation "xerces:xercesImpl:2.12.2"
implementation "androidx.security:security-crypto:1.1.0-alpha06"
implementation "com.google.crypto.tink:tink-android:1.15.0"
implementation "org.bitcoinj:bitcoinj-core:0.16.3"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package com.accountkit.reactnativesigner

import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.google.crypto.tink.BinaryKeysetWriter
import com.google.crypto.tink.HybridDecrypt
import com.google.crypto.tink.CleartextKeysetHandle
import com.google.crypto.tink.InsecureSecretKeyAccess
import com.google.crypto.tink.KeyTemplate
import com.google.crypto.tink.KeysetHandle
import com.google.crypto.tink.TinkJsonProtoKeysetFormat
import com.google.crypto.tink.config.TinkConfig
import com.google.crypto.tink.hybrid.HpkeParameters
import com.google.crypto.tink.hybrid.HpkePrivateKey
import com.google.crypto.tink.hybrid.internal.HpkeContext
import com.google.crypto.tink.hybrid.internal.HpkeKemKeyFactory
import com.google.crypto.tink.hybrid.internal.HpkePrimitiveFactory
import com.google.crypto.tink.proto.HpkePublicKey
import com.google.crypto.tink.subtle.Base64
import com.google.crypto.tink.subtle.EllipticCurves
import com.google.crypto.tink.util.Bytes
import com.google.crypto.tink.util.SecretBytes
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Expand All @@ -26,6 +36,7 @@ import java.nio.ByteBuffer
import java.security.KeyFactory
import java.security.Security
import java.security.Signature
import java.security.interfaces.ECPublicKey
import javax.xml.bind.DatatypeConverter


Expand Down Expand Up @@ -55,7 +66,7 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
*
* The reason we are not using the android key store for either of these things is because
* 1. For us to be able to import the private key in the bundle into the KeyStore, Turnkey
* has to return the key in a different format: https://developer.android.com/privacy-and-security/keystore#ImportingEncryptedKeys
* has to return the key in a different format (AFAIK): https://developer.android.com/privacy-and-security/keystore#ImportingEncryptedKeys
* 2. If we store the TEK in the KeyStore, then we have to roll our own HPKE decrypt function
* as there's no off the shelf solution (that I could find) to do the HPKE decryption. Rolling our own
* decryption feels wrong given we are not experts on this and don't have a good way to verify our
Expand All @@ -75,9 +86,21 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

// This allows us to do the HPKE decryption of the bundle
private val hpkeParams = HpkeParameters.builder()
.setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
.setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
.setAeadId(HpkeParameters.AeadId.AES_256_GCM)
.setVariant(HpkeParameters.Variant.NO_PREFIX)
.build()

init {
TinkConfig.register()

if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME).javaClass != BouncyCastleProvider::class.java) {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
}

if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(BouncyCastleProvider())
}
Expand All @@ -93,26 +116,23 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
if (existingPublicKey != null) {
return promise.resolve(existingPublicKey)
}
// This allows us to do the HPKE decryption of the bundle
val hpkeParams = HpkeParameters.builder()
.setKemId(HpkeParameters.KemId.DHKEM_P256_HKDF_SHA256)
.setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
.setAeadId(HpkeParameters.AeadId.AES_256_GCM)
.setVariant(HpkeParameters.Variant.NO_PREFIX)
.build()

// Generate a P256 key
val keyHandle = KeysetHandle.generateNew(hpkeParams)
val keyHandle = KeysetHandle.generateNew(KeyTemplate.createFrom(hpkeParams))

// Store the ephemeral key in encrypted shared preferences
sharedPreferences
.edit()
.putString(
TEK_STORAGE_KEY,
TinkJsonProtoKeysetFormat.serializeKeysetWithoutSecret(keyHandle)
TinkJsonProtoKeysetFormat.serializeKeyset(
keyHandle,
InsecureSecretKeyAccess.get()
)
)
.apply()
return promise.resolve(publicKeyToHex(keyHandle))

return promise.resolve(tekPublicKeyHex(keyHandle))
} catch (e: Exception) {
promise.reject(e)
}
Expand All @@ -125,7 +145,7 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
override fun publicKey(): String? {
val existingHandle = getRecipientKeyHandle() ?: return null

return publicKeyToHex(existingHandle)
return tekPublicKeyHex(existingHandle)
}

override fun injectCredentialBundle(bundle: String, promise: Promise) {
Expand All @@ -135,38 +155,58 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :

val decodedBundle = Base58.decodeChecked(bundle)
val buffer = ByteBuffer.wrap(decodedBundle)

// Turnkey bundle is first 33 bytes as the key and remaining the encrypted private key
// TODO: actually... this might have to be 32 looking at some of the code elsewhere
val ephemeralPublicKeyLength = 33
val ephemeralPublicKeyBytes = ByteArray(ephemeralPublicKeyLength)
buffer.get(ephemeralPublicKeyBytes)
val ephemeralPublicKey = EllipticCurves.getEcPublicKey(
EllipticCurves.CurveType.NIST_P256,
EllipticCurves.PointFormatType.COMPRESSED,
ephemeralPublicKeyBytes,
)
val uncompressedEphemeralKey = convertToUncompressedPublicKeyBytes(ephemeralPublicKey)

val ciphertext = ByteArray(buffer.remaining())
buffer.get(ciphertext)

val hybridDecrypt = tekHandle.getPrimitive(HybridDecrypt::class.java)
val context = ephemeralPublicKeyBytes + publicKeyToHex(tekHandle).toByteArray()
val decryptedKey =
hybridDecrypt.decrypt(ciphertext, context)
val aad = uncompressedEphemeralKey + DatatypeConverter.parseHexBinary(
tekPublicKeyHex(tekHandle)
)

// Why do we hve to do all this rather than doing:
// val hybridDecrypt = tekHandle.getPrimitive(HybridDecrypt::class.java)
// val decryptedKey = hybridDecrypt.decrypt(ciphertext, "turnkey_hpke".toByteArray())
// the hybridDecrypt.decrypt that google exposes doesn't allow us to pass in
// the aad that's needed to complete decryption
val recipient = HpkeContext.createRecipientContext(
convertToUncompressedPublicKeyBytes(ephemeralPublicKey),
HpkeKemKeyFactory.createPrivate(getHpkePrivateKeyFromKeysetHandle(tekHandle)),
HpkePrimitiveFactory.createKem(hpkeParams.kemId),
HpkePrimitiveFactory.createKdf(hpkeParams.kdfId),
HpkePrimitiveFactory.createAead(hpkeParams.aeadId),
"turnkey_hpke".toByteArray()
)

val decryptedKey = recipient.open(ciphertext, aad)

val (publicKeyBytes, privateKeyBytes) = privateKeyToKeyPair(decryptedKey)

sharedPreferences.edit()
.putString(
BUNDLE_PRIVATE_KEY,
DatatypeConverter.printHexBinary(privateKeyBytes).uppercase()
DatatypeConverter.printHexBinary(privateKeyBytes).lowercase()
)
.apply()

sharedPreferences.edit()
.putString(
BUNDLE_PUBLIC_KEY,
DatatypeConverter.printHexBinary(publicKeyBytes).uppercase()
DatatypeConverter.printHexBinary(publicKeyBytes).lowercase()
)
.apply()

return promise.resolve(true)
} catch (e: Exception) {
Log.e("error", "an error happened", e)
promise.reject(e)
}
}
Expand All @@ -183,7 +223,7 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :

val ecPrivateKey = EllipticCurves.getEcPrivateKey(
EllipticCurves.CurveType.NIST_P256,
signingKeyHex.toByteArray()
DatatypeConverter.parseHexBinary(signingKeyHex)
)

val signer = Signature.getInstance("SHA256withECDSA")
Expand All @@ -194,14 +234,14 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
val apiStamp = ApiStamp(
publicSigningKeyHex,
"SIGNATURE_SCHEME_TK_API_P256",
DatatypeConverter.printHexBinary(signature).uppercase()
DatatypeConverter.printHexBinary(signature)
)

val stamp = Arguments.createMap()
stamp.putString("stampHeaderName", "X-Stamp")
stamp.putString(
"stampHeaderValue",
Base64.encode(Json.encodeToString(apiStamp).toByteArray())
Base64.urlSafeEncode(Json.encodeToString(apiStamp).toByteArray())
)
return promise.resolve(stamp)
} catch (e: Exception) {
Expand All @@ -214,11 +254,12 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
return null;
}

return TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret(
return TinkJsonProtoKeysetFormat.parseKeyset(
sharedPreferences.getString(
TEK_STORAGE_KEY,
null
)
),
InsecureSecretKeyAccess.get()
)
}

Expand All @@ -234,22 +275,56 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
val pubSpec = ECPublicKeySpec(bcSpec.g.multiply(s).normalize(), bcSpec)
val keyFactory =
KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)

val ecPublicKey = EllipticCurves.getEcPublicKey(keyFactory.generatePublic(pubSpec).encoded)

// verify the key pair
EllipticCurves.validatePublicKey(ecPublicKey, ecPrivateKey)

return Pair(ecPublicKey.encoded, ecPrivateKey.encoded)
// compress it to match turnkey expectations
val compressedPublicKey = EllipticCurves.pointEncode(
EllipticCurves.CurveType.NIST_P256,
EllipticCurves.PointFormatType.COMPRESSED,
ecPublicKey.w
)
return Pair(compressedPublicKey, privateKey)
}

private fun publicKeyToHex(keyHandle: KeysetHandle): String {
val outputStream = ByteArrayOutputStream()
keyHandle.publicKeysetHandle.writeNoSecret(
BinaryKeysetWriter.withOutputStream(
outputStream
private fun tekPublicKeyHex(keyHandle: KeysetHandle): String {
val keySet = CleartextKeysetHandle.getKeyset(keyHandle.publicKeysetHandle)
val hpkePublicKey = HpkePublicKey.parseFrom(keySet.keyList[0].keyData.value)

val publicKeyBytes = hpkePublicKey.publicKey.toByteArray()
return DatatypeConverter.printHexBinary(publicKeyBytes)
}

private fun getHpkePrivateKeyFromKeysetHandle(keysetHandle: KeysetHandle): HpkePrivateKey {
val pkKs = CleartextKeysetHandle.getKeyset(keysetHandle)
val pkKeyData = pkKs.keyList[0].keyData
if (pkKeyData.typeUrl != "type.googleapis.com/google.crypto.tink.HpkePrivateKey") {
throw Error("invalid key type")
}

return HpkePrivateKey.create(
com.google.crypto.tink.hybrid.HpkePublicKey.create(
hpkeParams,
Bytes.copyFrom(DatatypeConverter.parseHexBinary(tekPublicKeyHex(keysetHandle))),
null
),
SecretBytes.copyFrom(
com.google.crypto.tink.proto.HpkePrivateKey.parseFrom(pkKeyData.value).privateKey.toByteArray(),
InsecureSecretKeyAccess.get()
)
)
return DatatypeConverter.printHexBinary(outputStream.toByteArray()).uppercase()
}

private fun convertToUncompressedPublicKeyBytes(ephemeralPublicKey: ECPublicKey): ByteArray {
val ecPoint = ephemeralPublicKey.w
return EllipticCurves.pointEncode(
EllipticCurves.CurveType.NIST_P256,
EllipticCurves.PointFormatType.UNCOMPRESSED,
ecPoint
)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ class ReactNativeSignerPackage : TurboReactPackage() {
NativeTEKStamperModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
true // isTurboModule
)
moduleInfos
}
Expand Down
1 change: 1 addition & 0 deletions account-kit/rn-signer/example/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"

/**
* This is the configuration block to customize your React Native Android app.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package accountkit.reactnativesigner.example

import android.app.Application
import com.accountkit.reactnativesigner.NativeTEKStamperModule
import com.accountkit.reactnativesigner.NativeTEKStamperSpec
import com.accountkit.reactnativesigner.ReactNativeSignerPackage
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
Expand All @@ -15,20 +18,21 @@ import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for
// example:
add(ReactNativeSignerPackage())
}

override fun getJSMainModuleName(): String = "index"
override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
Expand Down
7 changes: 6 additions & 1 deletion account-kit/rn-signer/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
"build:ios": "react-native build-ios --scheme ReactNativeSignerExample --mode Debug --extra-params \"-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO\""
},
"dependencies": {
"@account-kit/signer": "^4.3.0",
"react": "18.3.1",
"react-native": "0.76.1"
"react-native": "0.76.1",
"react-native-config": "^1.5.3",
"util": "^0.12.5",
"viem": "^2.21.41",
"zustand": "^5.0.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand Down
Loading

0 comments on commit 5c58383

Please sign in to comment.