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": [