From 5689df9cbabb7df5fa9aab15b6e39104185eb708 Mon Sep 17 00:00:00 2001 From: moldy Date: Tue, 5 Nov 2024 12:58:18 -0500 Subject: [PATCH] feat(rn-signer): add key pair generation in android --- account-kit/rn-signer/android/build.gradle | 3 + .../android/src/main/AndroidManifestNew.xml | 2 - .../NativeTEKStamperModule.kt | 130 ++++++++++++++++++ .../ReactNativeSignerModule.kt | 38 ----- .../ReactNativeSignerPackage.kt | 10 +- account-kit/rn-signer/example/src/App.tsx | 7 +- account-kit/rn-signer/package.json | 2 + 7 files changed, 142 insertions(+), 50 deletions(-) delete mode 100644 account-kit/rn-signer/android/src/main/AndroidManifestNew.xml create mode 100644 account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/NativeTEKStamperModule.kt delete mode 100644 account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerModule.kt diff --git a/account-kit/rn-signer/android/build.gradle b/account-kit/rn-signer/android/build.gradle index 8f27afada6..5f6c3290d3 100644 --- a/account-kit/rn-signer/android/build.gradle +++ b/account-kit/rn-signer/android/build.gradle @@ -111,6 +111,9 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "javax.xml.bind:jaxb-api:2.3.1" + implementation "androidx.security:security-crypto:1.1.0-alpha06" + implementation "com.google.crypto.tink:tink-android:1.15.0" } if (isNewArchitectureEnabled()) { diff --git a/account-kit/rn-signer/android/src/main/AndroidManifestNew.xml b/account-kit/rn-signer/android/src/main/AndroidManifestNew.xml deleted file mode 100644 index a2f47b6057..0000000000 --- a/account-kit/rn-signer/android/src/main/AndroidManifestNew.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/NativeTEKStamperModule.kt b/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/NativeTEKStamperModule.kt new file mode 100644 index 0000000000..3ffb1be22e --- /dev/null +++ b/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/NativeTEKStamperModule.kt @@ -0,0 +1,130 @@ +package com.accountkit.reactnativesigner + +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +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.KeysetHandle +import com.google.crypto.tink.TinkJsonProtoKeysetFormat +import com.google.crypto.tink.signature.EcdsaSignKeyManager +import com.google.crypto.tink.signature.SignatureConfig +import java.io.ByteArrayOutputStream +import javax.xml.bind.DatatypeConverter + +@ReactModule(name = NativeTEKStamperModule.NAME) +class NativeTEKStamperModule(reactContext: ReactApplicationContext) : + NativeTEKStamperSpec(reactContext) { + + private val TEK_STORAGE_KEY = "TEK_STORAGE_KEY" + private val context = reactContext + + /** + * We are using EncryptedSharedPreferences to store 2 pieces of data + * 1. the TEK keypair - this is the ephemeral key-pair that Turnkey will use + * to encrypt the bundle with + * 2. the decrypted private key for a session + * + * 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 + * 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 + * implementation (and I don't trust the ChatGPT output to be correct. Even if it is, there's no + * guarantee we can test all the edge cases since those are unknown unknowns) + * + * NOTE: this isn't too far off from how Turnkey recommends doing it in Swift + * https://github.com/tkhq/swift-sdk/blob/5817374a7cbd4c99b7ea90b170363dc2bf6c59b9/docs/email-auth.md#email-authentication + * + * The open question is if the storage of the decrypted private key is secure enough though + */ + private val masterKey = MasterKey.Builder(context.applicationContext) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .setUserAuthenticationRequired(false) + .build(); + + private val sharedPreferences = EncryptedSharedPreferences.create( + context, + "tek_stamper_shared_prefs", + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + override fun getName(): String { + return NAME + } + + override fun init(promise: Promise) { + // Register the ECDSA manager + SignatureConfig.register() + + try { + val existingPublicKey = publicKey() + if (existingPublicKey != null) { + return promise.resolve(existingPublicKey) + } + // Generate a P256 key + val keyHandle = KeysetHandle.generateNew(EcdsaSignKeyManager.ecdsaP256Template()) + + // Store the ephemeral key in encrypted shared preferences + sharedPreferences + .edit() + .putString( + TEK_STORAGE_KEY, + TinkJsonProtoKeysetFormat.serializeKeysetWithoutSecret(keyHandle) + ) + .apply() + return promise.resolve(publicKeyToHex(keyHandle)) + } catch (e: Exception) { + promise.reject(e) + } + } + + override fun clear() { + TODO("Not yet implemented") + } + + override fun publicKey(): String? { + val existingHandle = getRecipientKeyHandle() ?: return null + + return publicKeyToHex(existingHandle) + } + + override fun injectCredentialBundle(bundle: String?, promise: Promise) { + TODO("Not yet implemented") + } + + override fun stamp(payload: String?, promise: Promise) { + TODO("Not yet implemented") + } + + private fun getRecipientKeyHandle(): KeysetHandle? { + if (!sharedPreferences.contains(TEK_STORAGE_KEY)) { + return null; + } + + return TinkJsonProtoKeysetFormat.parseKeysetWithoutSecret( + sharedPreferences.getString( + TEK_STORAGE_KEY, + "{}" + ) + ) + } + + private fun publicKeyToHex(keyHandle: KeysetHandle): String { + val outputStream = ByteArrayOutputStream() + keyHandle.publicKeysetHandle.writeNoSecret( + BinaryKeysetWriter.withOutputStream( + outputStream + ) + ) + return DatatypeConverter.printHexBinary(outputStream.toByteArray()).uppercase() + } + + companion object { + const val NAME = "NativeTEKStamper" + } +} diff --git a/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerModule.kt b/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerModule.kt deleted file mode 100644 index 05f921849a..0000000000 --- a/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.accountkit.reactnativesigner - -import com.facebook.react.bridge.Promise -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.module.annotations.ReactModule - -@ReactModule(name = ReactNativeSignerModule.NAME) -class ReactNativeSignerModule(reactContext: ReactApplicationContext) : - NativeTEKStamperSpec(reactContext) { - - override fun getName(): String { - return NAME - } - - override fun init(promise: Promise?) { - TODO("Not yet implemented") - } - - override fun clear() { - TODO("Not yet implemented") - } - - override fun publicKey(): String? { - TODO("Not yet implemented") - } - - override fun injectCredentialBundle(bundle: String?, promise: Promise?) { - TODO("Not yet implemented") - } - - override fun stamp(payload: String?, promise: Promise?) { - TODO("Not yet implemented") - } - - companion object { - const val NAME = "ReactNativeSigner" - } -} diff --git a/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerPackage.kt b/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerPackage.kt index 7b137491c5..e96a9ba35b 100644 --- a/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerPackage.kt +++ b/account-kit/rn-signer/android/src/main/java/com/accountkit/reactnativesigner/ReactNativeSignerPackage.kt @@ -9,8 +9,8 @@ import java.util.HashMap class ReactNativeSignerPackage : TurboReactPackage() { override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { - return if (name == ReactNativeSignerModule.NAME) { - ReactNativeSignerModule(reactContext) + return if (name == NativeTEKStamperModule.NAME) { + NativeTEKStamperModule(reactContext) } else { null } @@ -19,9 +19,9 @@ class ReactNativeSignerPackage : TurboReactPackage() { override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { return ReactModuleInfoProvider { val moduleInfos: MutableMap = HashMap() - moduleInfos[ReactNativeSignerModule.NAME] = ReactModuleInfo( - ReactNativeSignerModule.NAME, - ReactNativeSignerModule.NAME, + moduleInfos[NativeTEKStamperModule.NAME] = ReactModuleInfo( + NativeTEKStamperModule.NAME, + NativeTEKStamperModule.NAME, false, // canOverrideExistingModule false, // needsEagerInit true, // hasConstants diff --git a/account-kit/rn-signer/example/src/App.tsx b/account-kit/rn-signer/example/src/App.tsx index 0b8079f2a6..a6c2ccbd04 100644 --- a/account-kit/rn-signer/example/src/App.tsx +++ b/account-kit/rn-signer/example/src/App.tsx @@ -1,12 +1,9 @@ -import { StyleSheet, View, Text } from "react-native"; -import { multiply } from "@account-kit/react-native-signer"; - -const result = multiply(3, 7); +import { StyleSheet, Text, View } from "react-native"; export default function App() { return ( - Result: {result} + Result: 0 ); } diff --git a/account-kit/rn-signer/package.json b/account-kit/rn-signer/package.json index b2446fc35c..3b34a2d9ba 100644 --- a/account-kit/rn-signer/package.json +++ b/account-kit/rn-signer/package.json @@ -39,7 +39,9 @@ "scripts": { "test": "jest", "typecheck": "tsc", + "build": "bob build && yarn typecheck && yarn build:android", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", + "build:android": "cd ./example && react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", "prepare": "bob build" }, "keywords": [