Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(rn): get login working e2e #1132

Merged
merged 1 commit into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading