@account-kit/react-native-signer 4.1.8-alpha → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/android/src/main/java/com/accountkit/reactnativesigner/NativeTEKStamperModule.kt +13 -181
- package/android/src/main/java/com/accountkit/reactnativesigner/{KeyExtensions.kt → core/KeyExtensions.kt} +1 -1
- package/android/src/main/java/com/accountkit/reactnativesigner/{TEKManager.kt → core/TEKManager.kt} +3 -2
- package/android/src/main/java/com/accountkit/reactnativesigner/core/TEKStamper.kt +199 -0
- package/android/src/main/java/com/accountkit/reactnativesigner/core/errors/NoInjectedBundleException.kt +3 -0
- package/android/src/main/java/com/accountkit/reactnativesigner/core/errors/NoTEKException.kt +3 -0
- package/android/src/main/java/com/accountkit/reactnativesigner/core/errors/StamperNotInitialized.kt +3 -0
- package/package.json +6 -5
- package/.github/actions/setup/action.yml +0 -27
- package/.github/workflows/ci.yml +0 -157
- package/.npmrc +0 -1
- package/.turbo/turbo-prepare.log +0 -21
- package/.watchmanconfig +0 -1
- package/babel.config.js +0 -5
- package/example/.bundle/config +0 -2
- package/example/.watchmanconfig +0 -1
- package/example/Gemfile +0 -9
- package/example/README.md +0 -79
- package/example/android/app/build.gradle +0 -133
- package/example/android/app/debug.keystore +0 -0
- package/example/android/app/proguard-rules.pro +0 -10
- package/example/android/app/src/debug/AndroidManifest.xml +0 -9
- package/example/android/app/src/main/AndroidManifest.xml +0 -17
- package/example/android/app/src/main/java/accountkit/reactnativesigner/example/MainActivity.kt +0 -27
- package/example/android/app/src/main/java/accountkit/reactnativesigner/example/MainApplication.kt +0 -48
- package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/values/strings.xml +0 -3
- package/example/android/app/src/main/res/values/styles.xml +0 -9
- package/example/android/build.gradle +0 -21
- package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/example/android/gradle.properties +0 -41
- package/example/android/gradlew +0 -252
- package/example/android/gradlew.bat +0 -94
- package/example/android/settings.gradle +0 -6
- package/example/app.json +0 -4
- package/example/babel.config.js +0 -12
- package/example/index.js +0 -6
- package/example/ios/.xcode.env +0 -11
- package/example/ios/File.swift +0 -6
- package/example/ios/Podfile +0 -47
- package/example/ios/ReactNativeSignerExample/AppDelegate.h +0 -6
- package/example/ios/ReactNativeSignerExample/AppDelegate.mm +0 -31
- package/example/ios/ReactNativeSignerExample/Images.xcassets/AppIcon.appiconset/Contents.json +0 -53
- package/example/ios/ReactNativeSignerExample/Images.xcassets/Contents.json +0 -6
- package/example/ios/ReactNativeSignerExample/Info.plist +0 -52
- package/example/ios/ReactNativeSignerExample/LaunchScreen.storyboard +0 -47
- package/example/ios/ReactNativeSignerExample/PrivacyInfo.xcprivacy +0 -37
- package/example/ios/ReactNativeSignerExample/main.m +0 -10
- package/example/ios/ReactNativeSignerExample-Bridging-Header.h +0 -3
- package/example/ios/ReactNativeSignerExample.xcodeproj/project.pbxproj +0 -690
- package/example/ios/ReactNativeSignerExample.xcodeproj/xcshareddata/xcschemes/ReactNativeSignerExample.xcscheme +0 -98
- package/example/ios/ReactNativeSignerExampleTests/Info.plist +0 -24
- package/example/ios/ReactNativeSignerExampleTests/ReactNativeSignerExampleTests.m +0 -66
- package/example/jest.config.js +0 -3
- package/example/metro.config.js +0 -22
- package/example/package.json +0 -56
- package/example/react-native.config.js +0 -15
- package/example/redirect-server/index.ts +0 -19
- package/example/src/App.tsx +0 -30
- package/example/src/screens/Home.tsx +0 -149
- package/example/turbo.json +0 -38
- package/react-native.config.js +0 -11
- package/tsconfig.build.json +0 -4
- package/tsconfig.json +0 -30
- package/turbo.json +0 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Alchemy Insights, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,97 +1,16 @@
|
|
|
1
1
|
package com.accountkit.reactnativesigner
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import androidx.security.crypto.MasterKey
|
|
3
|
+
import com.accountkit.reactnativesigner.core.TEKStamper
|
|
5
4
|
import com.facebook.react.bridge.Arguments
|
|
6
5
|
import com.facebook.react.bridge.Promise
|
|
7
6
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
7
|
import com.facebook.react.module.annotations.ReactModule
|
|
9
|
-
import com.google.crypto.tink.config.TinkConfig
|
|
10
|
-
import com.google.crypto.tink.subtle.Base64
|
|
11
|
-
import com.google.crypto.tink.subtle.EllipticCurves
|
|
12
|
-
import java.nio.ByteBuffer
|
|
13
|
-
import java.security.KeyFactory
|
|
14
|
-
import java.security.Security
|
|
15
|
-
import java.security.Signature
|
|
16
|
-
import kotlinx.serialization.Serializable
|
|
17
|
-
import kotlinx.serialization.encodeToString
|
|
18
|
-
import kotlinx.serialization.json.Json
|
|
19
|
-
import org.bitcoinj.core.Base58
|
|
20
|
-
import org.bouncycastle.jce.ECNamedCurveTable
|
|
21
|
-
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
22
|
-
import org.bouncycastle.jce.spec.ECPublicKeySpec
|
|
23
|
-
|
|
24
|
-
@Serializable
|
|
25
|
-
data class ApiStamp(val publicKey: String, val scheme: String, val signature: String)
|
|
26
|
-
|
|
27
|
-
private const val BUNDLE_PRIVATE_KEY = "BUNDLE_PRIVATE_KEY"
|
|
28
|
-
private const val BUNDLE_PUBLIC_KEY = "BUNDLE_PUBLIC_KEY"
|
|
29
8
|
|
|
30
9
|
@ReactModule(name = NativeTEKStamperModule.NAME)
|
|
31
10
|
class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
|
|
32
11
|
NativeTEKStamperSpec(reactContext) {
|
|
33
12
|
|
|
34
|
-
private val
|
|
35
|
-
|
|
36
|
-
// This is how the docs for EncryptedSharedPreferences recommend creating this setup
|
|
37
|
-
// NOTE: we can further customize the permissions around accessing this master key and the keys
|
|
38
|
-
// used to generate it by using the .setKeyGenParameterSpec() method on this builder
|
|
39
|
-
// this would allow us to further specify the access requirements to this key
|
|
40
|
-
//
|
|
41
|
-
// we should explore the best practices on how to do this once we reach a phase of further
|
|
42
|
-
// cleanup
|
|
43
|
-
private val masterKey =
|
|
44
|
-
MasterKey.Builder(context.applicationContext)
|
|
45
|
-
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
46
|
-
// requires that the phone be unlocked
|
|
47
|
-
.setUserAuthenticationRequired(false)
|
|
48
|
-
.build()
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* We are using EncryptedSharedPreferences to store 2 pieces of data
|
|
52
|
-
* 1. the TEK keypair - this is the ephemeral key-pair that Turnkey will use to encrypt the
|
|
53
|
-
* bundle with
|
|
54
|
-
* 2. the decrypted private key for a session
|
|
55
|
-
*
|
|
56
|
-
* The reason we are not using the android key store for either of these things is because
|
|
57
|
-
* 1. For us to be able to import the private key in the bundle into the KeyStore, Turnkey has
|
|
58
|
-
* to return the key in a different format (AFAIK):
|
|
59
|
-
* https://developer.android.com/privacy-and-security/keystore#ImportingEncryptedKeys
|
|
60
|
-
* 2. If we store the TEK in the KeyStore, then we have to roll our own HPKE decrypt function as
|
|
61
|
-
* there's no off the shelf solution (that I could find) to do the HPKE decryption. Rolling our
|
|
62
|
-
* own decryption feels wrong given we are not experts on this and don't have a good way to
|
|
63
|
-
* verify our implementation (and I don't trust the ChatGPT output to be correct. Even if it is,
|
|
64
|
-
* there's no guarantee we can test all the edge cases since those are unknown unknowns)
|
|
65
|
-
*
|
|
66
|
-
* NOTE: this isn't too far off from how Turnkey recommends doing it in Swift
|
|
67
|
-
* https://github.com/tkhq/swift-sdk/blob/5817374a7cbd4c99b7ea90b170363dc2bf6c59b9/docs/email-auth.md#email-authentication
|
|
68
|
-
*
|
|
69
|
-
* The open question is if the storage of the decrypted private key is secure enough though
|
|
70
|
-
*/
|
|
71
|
-
private val sharedPreferences =
|
|
72
|
-
EncryptedSharedPreferences.create(
|
|
73
|
-
context,
|
|
74
|
-
"tek_stamper_shared_prefs",
|
|
75
|
-
masterKey,
|
|
76
|
-
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
77
|
-
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
private val tekManager = HpkeTEKManager(sharedPreferences)
|
|
81
|
-
|
|
82
|
-
init {
|
|
83
|
-
TinkConfig.register()
|
|
84
|
-
|
|
85
|
-
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME).javaClass !=
|
|
86
|
-
BouncyCastleProvider::class.java
|
|
87
|
-
) {
|
|
88
|
-
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
|
92
|
-
Security.addProvider(BouncyCastleProvider())
|
|
93
|
-
}
|
|
94
|
-
}
|
|
13
|
+
private val stamper = TEKStamper(reactContext.applicationContext)
|
|
95
14
|
|
|
96
15
|
override fun getName(): String {
|
|
97
16
|
return NAME
|
|
@@ -99,65 +18,25 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
|
|
|
99
18
|
|
|
100
19
|
override fun init(promise: Promise) {
|
|
101
20
|
try {
|
|
102
|
-
val tekPublicKey =
|
|
21
|
+
val tekPublicKey = stamper.init()
|
|
103
22
|
|
|
104
|
-
return promise.resolve(tekPublicKey
|
|
23
|
+
return promise.resolve(tekPublicKey)
|
|
105
24
|
} catch (e: Exception) {
|
|
106
25
|
promise.reject(e)
|
|
107
26
|
}
|
|
108
27
|
}
|
|
109
28
|
|
|
110
29
|
override fun clear() {
|
|
111
|
-
|
|
30
|
+
stamper.clear()
|
|
112
31
|
}
|
|
113
32
|
|
|
114
33
|
override fun publicKey(): String? {
|
|
115
|
-
return
|
|
34
|
+
return stamper.publicKey()
|
|
116
35
|
}
|
|
117
36
|
|
|
118
37
|
override fun injectCredentialBundle(bundle: String, promise: Promise) {
|
|
119
38
|
try {
|
|
120
|
-
|
|
121
|
-
tekManager.publicKey()
|
|
122
|
-
?: return promise.reject(Exception("Stamper has not been initialized"))
|
|
123
|
-
|
|
124
|
-
val decodedBundle = Base58.decodeChecked(bundle)
|
|
125
|
-
val buffer = ByteBuffer.wrap(decodedBundle)
|
|
126
|
-
val ephemeralPublicKeyLength = 33
|
|
127
|
-
val ephemeralPublicKeyBytes = ByteArray(ephemeralPublicKeyLength)
|
|
128
|
-
buffer.get(ephemeralPublicKeyBytes)
|
|
129
|
-
val ephemeralPublicKey =
|
|
130
|
-
EllipticCurves.getEcPublicKey(
|
|
131
|
-
EllipticCurves.CurveType.NIST_P256,
|
|
132
|
-
EllipticCurves.PointFormatType.COMPRESSED,
|
|
133
|
-
ephemeralPublicKeyBytes,
|
|
134
|
-
)
|
|
135
|
-
.toBytes(EllipticCurves.PointFormatType.UNCOMPRESSED)
|
|
136
|
-
|
|
137
|
-
val ciphertext = ByteArray(buffer.remaining())
|
|
138
|
-
buffer.get(ciphertext)
|
|
139
|
-
|
|
140
|
-
val aad = ephemeralPublicKey + tekPublicKey.toByteArray()
|
|
141
|
-
|
|
142
|
-
val decryptedKey =
|
|
143
|
-
tekManager.hpkeDecrypt(
|
|
144
|
-
ephemeralPublicKey,
|
|
145
|
-
ciphertext,
|
|
146
|
-
"turnkey_hpke".toByteArray(),
|
|
147
|
-
aad
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
val (publicKeyBytes, privateKeyBytes) = privateKeyToKeyPair(decryptedKey)
|
|
151
|
-
|
|
152
|
-
sharedPreferences
|
|
153
|
-
.edit()
|
|
154
|
-
.putString(BUNDLE_PRIVATE_KEY, privateKeyBytes.toHex().lowercase())
|
|
155
|
-
.apply()
|
|
156
|
-
|
|
157
|
-
sharedPreferences
|
|
158
|
-
.edit()
|
|
159
|
-
.putString(BUNDLE_PUBLIC_KEY, publicKeyBytes.toHex().lowercase())
|
|
160
|
-
.apply()
|
|
39
|
+
stamper.injectCredentialBundle(bundle)
|
|
161
40
|
|
|
162
41
|
return promise.resolve(true)
|
|
163
42
|
} catch (e: Exception) {
|
|
@@ -167,67 +46,20 @@ class NativeTEKStamperModule(reactContext: ReactApplicationContext) :
|
|
|
167
46
|
|
|
168
47
|
override fun stamp(payload: String, promise: Promise) {
|
|
169
48
|
try {
|
|
170
|
-
val
|
|
171
|
-
sharedPreferences.getString(BUNDLE_PRIVATE_KEY, null)
|
|
172
|
-
?: return promise.reject(
|
|
173
|
-
Exception("No injected bundle, did you complete auth?")
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
val publicSigningKeyHex =
|
|
177
|
-
sharedPreferences.getString(BUNDLE_PUBLIC_KEY, null)
|
|
178
|
-
?: return promise.reject(
|
|
179
|
-
Exception("No injected bundle, did you complete auth?")
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
val ecPrivateKey =
|
|
183
|
-
EllipticCurves.getEcPrivateKey(
|
|
184
|
-
EllipticCurves.CurveType.NIST_P256,
|
|
185
|
-
signingKeyHex.fromHex()
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
val signer = Signature.getInstance("SHA256withECDSA")
|
|
189
|
-
signer.initSign(ecPrivateKey)
|
|
190
|
-
signer.update(payload.toByteArray())
|
|
191
|
-
val signature = signer.sign()
|
|
192
|
-
|
|
193
|
-
val apiStamp =
|
|
194
|
-
ApiStamp(publicSigningKeyHex, "SIGNATURE_SCHEME_TK_API_P256", signature.toHex())
|
|
49
|
+
val stamp = stamper.stamp(payload)
|
|
195
50
|
|
|
196
|
-
val
|
|
197
|
-
|
|
198
|
-
|
|
51
|
+
val response = Arguments.createMap()
|
|
52
|
+
response.putString("stampHeaderName", stamp.stampHeaderName)
|
|
53
|
+
response.putString(
|
|
199
54
|
"stampHeaderValue",
|
|
200
|
-
|
|
55
|
+
stamp.stampHeaderValue
|
|
201
56
|
)
|
|
202
|
-
return promise.resolve(
|
|
57
|
+
return promise.resolve(response)
|
|
203
58
|
} catch (e: Exception) {
|
|
204
59
|
promise.reject(e)
|
|
205
60
|
}
|
|
206
61
|
}
|
|
207
62
|
|
|
208
|
-
private fun privateKeyToKeyPair(privateKey: ByteArray): Pair<ByteArray, ByteArray> {
|
|
209
|
-
val ecPrivateKey =
|
|
210
|
-
EllipticCurves.getEcPrivateKey(EllipticCurves.CurveType.NIST_P256, privateKey)
|
|
211
|
-
|
|
212
|
-
// compute the public key
|
|
213
|
-
val s = ecPrivateKey.s
|
|
214
|
-
val bcSpec = ECNamedCurveTable.getParameterSpec("secp256r1")
|
|
215
|
-
val pubSpec = ECPublicKeySpec(bcSpec.g.multiply(s).normalize(), bcSpec)
|
|
216
|
-
val keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
|
|
217
|
-
|
|
218
|
-
val ecPublicKey = EllipticCurves.getEcPublicKey(keyFactory.generatePublic(pubSpec).encoded)
|
|
219
|
-
|
|
220
|
-
// verify the key pair
|
|
221
|
-
EllipticCurves.validatePublicKey(ecPublicKey, ecPrivateKey)
|
|
222
|
-
|
|
223
|
-
// compress it to match turnkey expectations
|
|
224
|
-
val compressedPublicKey =
|
|
225
|
-
ecPublicKey.toBytes(
|
|
226
|
-
EllipticCurves.PointFormatType.COMPRESSED,
|
|
227
|
-
)
|
|
228
|
-
return Pair(compressedPublicKey, privateKey)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
63
|
companion object {
|
|
232
64
|
const val NAME = "NativeTEKStamper"
|
|
233
65
|
}
|
package/android/src/main/java/com/accountkit/reactnativesigner/{TEKManager.kt → core/TEKManager.kt}
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
package com.accountkit.reactnativesigner
|
|
1
|
+
package com.accountkit.reactnativesigner.core
|
|
2
2
|
|
|
3
3
|
import android.content.SharedPreferences
|
|
4
|
+
import com.accountkit.reactnativesigner.core.errors.NoTEKException
|
|
4
5
|
import com.google.crypto.tink.InsecureSecretKeyAccess
|
|
5
6
|
import com.google.crypto.tink.KeyTemplate
|
|
6
7
|
import com.google.crypto.tink.KeysetHandle
|
|
@@ -31,7 +32,7 @@ class HpkeTEKManager(private val sharedPreferences: SharedPreferences) {
|
|
|
31
32
|
// val decryptedKey = hybridDecrypt.decrypt(ciphertext, "turnkey_hpke".toByteArray())
|
|
32
33
|
// the hybridDecrypt.decrypt that google exposes doesn't allow us to pass in
|
|
33
34
|
// the aad that's needed to complete decryption
|
|
34
|
-
val keyHandle = getKeysetHandle() ?: throw
|
|
35
|
+
val keyHandle = getKeysetHandle() ?: throw NoTEKException()
|
|
35
36
|
|
|
36
37
|
val recipient = HpkeContext.createRecipientContext(
|
|
37
38
|
encapsulatePublicKey,
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
package com.accountkit.reactnativesigner.core
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.security.crypto.EncryptedSharedPreferences
|
|
5
|
+
import androidx.security.crypto.MasterKey
|
|
6
|
+
import com.accountkit.reactnativesigner.core.errors.NoInjectedBundleException
|
|
7
|
+
import com.accountkit.reactnativesigner.core.errors.StamperNotInitializedException
|
|
8
|
+
import com.google.crypto.tink.config.TinkConfig
|
|
9
|
+
import com.google.crypto.tink.subtle.Base64
|
|
10
|
+
import com.google.crypto.tink.subtle.EllipticCurves
|
|
11
|
+
import kotlinx.serialization.Serializable
|
|
12
|
+
import kotlinx.serialization.encodeToString
|
|
13
|
+
import kotlinx.serialization.json.Json
|
|
14
|
+
import org.bitcoinj.core.Base58
|
|
15
|
+
import org.bouncycastle.jce.ECNamedCurveTable
|
|
16
|
+
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
17
|
+
import org.bouncycastle.jce.spec.ECPublicKeySpec
|
|
18
|
+
import java.nio.ByteBuffer
|
|
19
|
+
import java.security.KeyFactory
|
|
20
|
+
import java.security.Security
|
|
21
|
+
import java.security.Signature
|
|
22
|
+
|
|
23
|
+
@Serializable
|
|
24
|
+
data class ApiStamp(val publicKey: String, val scheme: String, val signature: String)
|
|
25
|
+
|
|
26
|
+
data class Stamp(val stampHeaderName: String, val stampHeaderValue: String)
|
|
27
|
+
|
|
28
|
+
private const val BUNDLE_PRIVATE_KEY = "BUNDLE_PRIVATE_KEY"
|
|
29
|
+
private const val BUNDLE_PUBLIC_KEY = "BUNDLE_PUBLIC_KEY"
|
|
30
|
+
|
|
31
|
+
class TEKStamper(context: Context) {
|
|
32
|
+
// This is how the docs for EncryptedSharedPreferences recommend creating this setup
|
|
33
|
+
// NOTE: we can further customize the permissions around accessing this master key and the keys
|
|
34
|
+
// used to generate it by using the .setKeyGenParameterSpec() method on this builder
|
|
35
|
+
// this would allow us to further specify the access requirements to this key
|
|
36
|
+
//
|
|
37
|
+
// we should explore the best practices on how to do this once we reach a phase of further
|
|
38
|
+
// cleanup
|
|
39
|
+
private val masterKey =
|
|
40
|
+
MasterKey.Builder(context.applicationContext)
|
|
41
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
42
|
+
// requires that the phone be unlocked
|
|
43
|
+
.setUserAuthenticationRequired(false)
|
|
44
|
+
.build()
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* We are using EncryptedSharedPreferences to store 2 pieces of data
|
|
48
|
+
* 1. the TEK keypair - this is the ephemeral key-pair that Turnkey will use to encrypt the
|
|
49
|
+
* bundle with
|
|
50
|
+
* 2. the decrypted private key for a session
|
|
51
|
+
*
|
|
52
|
+
* The reason we are not using the android key store for either of these things is because
|
|
53
|
+
* 1. For us to be able to import the private key in the bundle into the KeyStore, Turnkey has
|
|
54
|
+
* to return the key in a different format (AFAIK):
|
|
55
|
+
* https://developer.android.com/privacy-and-security/keystore#ImportingEncryptedKeys
|
|
56
|
+
* 2. If we store the TEK in the KeyStore, then we have to roll our own HPKE decrypt function as
|
|
57
|
+
* there's no off the shelf solution (that I could find) to do the HPKE decryption. Rolling our
|
|
58
|
+
* own decryption feels wrong given we are not experts on this and don't have a good way to
|
|
59
|
+
* verify our implementation (and I don't trust the ChatGPT output to be correct. Even if it is,
|
|
60
|
+
* there's no guarantee we can test all the edge cases since those are unknown unknowns)
|
|
61
|
+
*
|
|
62
|
+
* NOTE: this isn't too far off from how Turnkey recommends doing it in Swift
|
|
63
|
+
* https://github.com/tkhq/swift-sdk/blob/5817374a7cbd4c99b7ea90b170363dc2bf6c59b9/docs/email-auth.md#email-authentication
|
|
64
|
+
*
|
|
65
|
+
* The open question is if the storage of the decrypted private key is secure enough though
|
|
66
|
+
*/
|
|
67
|
+
private val sharedPreferences =
|
|
68
|
+
EncryptedSharedPreferences.create(
|
|
69
|
+
context,
|
|
70
|
+
"tek_stamper_shared_prefs",
|
|
71
|
+
masterKey,
|
|
72
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
73
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
private val tekManager = HpkeTEKManager(sharedPreferences)
|
|
77
|
+
|
|
78
|
+
init {
|
|
79
|
+
TinkConfig.register()
|
|
80
|
+
|
|
81
|
+
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME).javaClass !=
|
|
82
|
+
BouncyCastleProvider::class.java
|
|
83
|
+
) {
|
|
84
|
+
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
|
88
|
+
Security.addProvider(BouncyCastleProvider())
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun init(): String {
|
|
93
|
+
return tekManager.createTEK().toHex()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fun clear() {
|
|
97
|
+
sharedPreferences.edit().clear().apply()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun publicKey(): String? {
|
|
101
|
+
return tekManager.publicKeyHex()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fun injectCredentialBundle(bundle: String) {
|
|
105
|
+
val tekPublicKey =
|
|
106
|
+
tekManager.publicKey()
|
|
107
|
+
?: throw StamperNotInitializedException()
|
|
108
|
+
|
|
109
|
+
val decodedBundle = Base58.decodeChecked(bundle)
|
|
110
|
+
val buffer = ByteBuffer.wrap(decodedBundle)
|
|
111
|
+
val ephemeralPublicKeyLength = 33
|
|
112
|
+
val ephemeralPublicKeyBytes = ByteArray(ephemeralPublicKeyLength)
|
|
113
|
+
buffer.get(ephemeralPublicKeyBytes)
|
|
114
|
+
val ephemeralPublicKey =
|
|
115
|
+
EllipticCurves.getEcPublicKey(
|
|
116
|
+
EllipticCurves.CurveType.NIST_P256,
|
|
117
|
+
EllipticCurves.PointFormatType.COMPRESSED,
|
|
118
|
+
ephemeralPublicKeyBytes,
|
|
119
|
+
)
|
|
120
|
+
.toBytes(EllipticCurves.PointFormatType.UNCOMPRESSED)
|
|
121
|
+
|
|
122
|
+
val ciphertext = ByteArray(buffer.remaining())
|
|
123
|
+
buffer.get(ciphertext)
|
|
124
|
+
|
|
125
|
+
val aad = ephemeralPublicKey + tekPublicKey.toByteArray()
|
|
126
|
+
|
|
127
|
+
val decryptedKey =
|
|
128
|
+
tekManager.hpkeDecrypt(
|
|
129
|
+
ephemeralPublicKey,
|
|
130
|
+
ciphertext,
|
|
131
|
+
"turnkey_hpke".toByteArray(),
|
|
132
|
+
aad
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
val (publicKeyBytes, privateKeyBytes) = privateKeyToKeyPair(decryptedKey)
|
|
136
|
+
|
|
137
|
+
sharedPreferences
|
|
138
|
+
.edit()
|
|
139
|
+
.putString(BUNDLE_PRIVATE_KEY, privateKeyBytes.toHex().lowercase())
|
|
140
|
+
.apply()
|
|
141
|
+
|
|
142
|
+
sharedPreferences
|
|
143
|
+
.edit()
|
|
144
|
+
.putString(BUNDLE_PUBLIC_KEY, publicKeyBytes.toHex().lowercase())
|
|
145
|
+
.apply()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun stamp(payload: String): Stamp {
|
|
149
|
+
val signingKeyHex =
|
|
150
|
+
sharedPreferences.getString(BUNDLE_PRIVATE_KEY, null)
|
|
151
|
+
?: throw NoInjectedBundleException()
|
|
152
|
+
|
|
153
|
+
val publicSigningKeyHex =
|
|
154
|
+
sharedPreferences.getString(BUNDLE_PUBLIC_KEY, null)
|
|
155
|
+
?: throw NoInjectedBundleException()
|
|
156
|
+
|
|
157
|
+
val ecPrivateKey =
|
|
158
|
+
EllipticCurves.getEcPrivateKey(
|
|
159
|
+
EllipticCurves.CurveType.NIST_P256,
|
|
160
|
+
signingKeyHex.fromHex()
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
val signer = Signature.getInstance("SHA256withECDSA")
|
|
164
|
+
signer.initSign(ecPrivateKey)
|
|
165
|
+
signer.update(payload.toByteArray())
|
|
166
|
+
val signature = signer.sign()
|
|
167
|
+
|
|
168
|
+
val apiStamp =
|
|
169
|
+
ApiStamp(publicSigningKeyHex, "SIGNATURE_SCHEME_TK_API_P256", signature.toHex())
|
|
170
|
+
|
|
171
|
+
return Stamp(
|
|
172
|
+
"X-Stamp",
|
|
173
|
+
Base64.urlSafeEncode(Json.encodeToString(apiStamp).toByteArray())
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private fun privateKeyToKeyPair(privateKey: ByteArray): Pair<ByteArray, ByteArray> {
|
|
178
|
+
val ecPrivateKey =
|
|
179
|
+
EllipticCurves.getEcPrivateKey(EllipticCurves.CurveType.NIST_P256, privateKey)
|
|
180
|
+
|
|
181
|
+
// compute the public key
|
|
182
|
+
val s = ecPrivateKey.s
|
|
183
|
+
val bcSpec = ECNamedCurveTable.getParameterSpec("secp256r1")
|
|
184
|
+
val pubSpec = ECPublicKeySpec(bcSpec.g.multiply(s).normalize(), bcSpec)
|
|
185
|
+
val keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
|
|
186
|
+
|
|
187
|
+
val ecPublicKey = EllipticCurves.getEcPublicKey(keyFactory.generatePublic(pubSpec).encoded)
|
|
188
|
+
|
|
189
|
+
// verify the key pair
|
|
190
|
+
EllipticCurves.validatePublicKey(ecPublicKey, ecPrivateKey)
|
|
191
|
+
|
|
192
|
+
// compress it to match turnkey expectations
|
|
193
|
+
val compressedPublicKey =
|
|
194
|
+
ecPublicKey.toBytes(
|
|
195
|
+
EllipticCurves.PointFormatType.COMPRESSED,
|
|
196
|
+
)
|
|
197
|
+
return Pair(compressedPublicKey, privateKey)
|
|
198
|
+
}
|
|
199
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@account-kit/react-native-signer",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.6.1",
|
|
4
4
|
"description": "React Native compatible Account Kit signer",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/commonjs/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"test": "jest",
|
|
41
41
|
"typecheck": "tsc",
|
|
42
|
-
"build
|
|
42
|
+
"build": "yarn typecheck",
|
|
43
43
|
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
44
44
|
"prepare": "bob build"
|
|
45
45
|
},
|
|
@@ -146,8 +146,9 @@
|
|
|
146
146
|
"version": "0.42.2"
|
|
147
147
|
},
|
|
148
148
|
"dependencies": {
|
|
149
|
-
"@aa-sdk/core": "^4.
|
|
150
|
-
"@account-kit/signer": "^4.
|
|
149
|
+
"@aa-sdk/core": "^4.6.1",
|
|
150
|
+
"@account-kit/signer": "^4.6.1",
|
|
151
151
|
"viem": "^2.21.40"
|
|
152
|
-
}
|
|
152
|
+
},
|
|
153
|
+
"gitHead": "2a88216fe9074fe72e08ddf0a1bca68c19388a8e"
|
|
153
154
|
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
name: Setup
|
|
2
|
-
description: Setup Node.js and install dependencies
|
|
3
|
-
|
|
4
|
-
runs:
|
|
5
|
-
using: composite
|
|
6
|
-
steps:
|
|
7
|
-
- name: Setup Node.js
|
|
8
|
-
uses: actions/setup-node@v3
|
|
9
|
-
with:
|
|
10
|
-
node-version-file: .nvmrc
|
|
11
|
-
|
|
12
|
-
- name: Cache dependencies
|
|
13
|
-
id: yarn-cache
|
|
14
|
-
uses: actions/cache@v3
|
|
15
|
-
with:
|
|
16
|
-
path: |
|
|
17
|
-
**/node_modules
|
|
18
|
-
.yarn/install-state.gz
|
|
19
|
-
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }}
|
|
20
|
-
restore-keys: |
|
|
21
|
-
${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
|
|
22
|
-
${{ runner.os }}-yarn-
|
|
23
|
-
|
|
24
|
-
- name: Install dependencies
|
|
25
|
-
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
|
26
|
-
run: yarn install --immutable
|
|
27
|
-
shell: bash
|