@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.3 → 1.0.0-canary.4

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/README.md CHANGED
@@ -1,6 +1,12 @@
1
- # 🔑 react-native-passkey-autofill
1
+ # @algorandfoundation/react-native-passkey-autofill
2
2
 
3
- Passkey AutoFill for React Native using DP256.
3
+ <p align="center">
4
+ <img src="./example/assets/banner.png" width="100%" />
5
+ </p>
6
+
7
+ <p align="center">
8
+ Passkey AutoFill for React Native using DP256.
9
+ </p>
4
10
 
5
11
  # 🚀 Get Started
6
12
 
@@ -9,7 +15,7 @@ For bare React Native projects, you must ensure that you have [installed and con
9
15
  ### Add the package to your npm dependencies
10
16
 
11
17
  ```bash
12
- npm install react-native-passkey-autofill
18
+ npm install @algorandfoundation/react-native-passkey-autofill
13
19
  ```
14
20
 
15
21
  ### Configure for Android
@@ -25,7 +31,7 @@ If you are using Expo, you can configure the plugin in your `app.json` or `app.c
25
31
  "expo": {
26
32
  "plugins": [
27
33
  [
28
- "react-native-passkey-autofill",
34
+ "@algorandfoundation/react-native-passkey-autofill",
29
35
  {
30
36
  "site": "https://your-fido-server.com",
31
37
  "label": "My Custom Credential Provider"
@@ -60,17 +66,21 @@ await ReactNativePasskeyAutofill.configureIntentActions(
60
66
  await ReactNativePasskeyAutofill.clearCredentials();
61
67
  ```
62
68
 
69
+ ## 📱 Example App
70
+
71
+ The [example](./example) app demonstrates how to integrate this module with a full wallet implementation.
72
+
63
73
  # 🤝 Contributing
64
74
 
65
75
  Contributions are very welcome! Please refer to guidelines described in the [contributing guide](./CONTRIBUTING.md).
66
76
 
67
77
  # 💖 Acknowledgements
68
78
 
69
- This has been the commination of many different efforts and ideas. We would like to thank the following individuals and organizations for their contributions:
79
+ This has been the culmination of many different efforts and ideas. We would like to thank the following individuals and organizations for their contributions:
70
80
 
71
81
  - [Bruno Martins](https://github.com/bmartins) the architect at [Algorand Foundation](https://github.com/algorandfoundation) for conceptualizing and guiding the project.
72
82
  - [HashMapsData2Value](https://github.com/HashMapsData2Value) for his guidance and support in DP256 and XHD and his work on the native autofill libraries.
73
- - [Will Beaumont](https://github.com/mjbeau) for working though integration within the Pera wallet
83
+ - [Will Beaumont](https://github.com/mjbeau) for working through integration within the Pera wallet
74
84
  - [Michael T Chuang](https://github.com/michaeltchuang) for his work in KMP integrations and client libraries.
75
85
 
76
86
  # 📄 License
@@ -6,7 +6,12 @@ import android.content.Intent
6
6
  import android.os.Build
7
7
  import android.os.Bundle
8
8
  import android.util.Log
9
+ import android.graphics.Color
10
+ import android.graphics.Typeface
11
+ import android.view.Gravity
9
12
  import android.widget.Button
13
+ import android.widget.ImageView
14
+ import android.widget.LinearLayout
10
15
  import android.widget.TextView
11
16
  import androidx.annotation.RequiresApi
12
17
  import androidx.appcompat.app.AppCompatActivity
@@ -31,61 +36,174 @@ class CreatePasskeyActivity : AppCompatActivity() {
31
36
  const val TAG = "CreatePasskeyActivity"
32
37
  }
33
38
 
34
- private var origin: String = "unknown-origin"
39
+ private var origin: String = "unknown-origin" // Derivation origin (rpId or hash)
40
+ private var displayOrigin: String = "unknown-origin"
35
41
  private var userHandle: String = "unknown-user"
36
42
  private var userName: String = "User"
37
43
  private var userId: String = "unknown-id"
44
+ private var bundleRequestJson: String? = null
38
45
  private var request: ProviderCreateCredentialRequest? = null
39
46
 
40
47
  override fun onCreate(savedInstanceState: Bundle?) {
41
48
  super.onCreate(savedInstanceState)
42
49
 
43
50
  request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
51
+ val credentialData = intent.getBundleExtra("CREDENTIAL_DATA")
52
+
53
+ if (credentialData != null) {
54
+ bundleRequestJson = credentialData.getString("requestJson")
55
+ }
56
+
44
57
  if (request != null) {
45
58
  try {
59
+ val systemOrigin = credentialRepository.getOrigin(request!!.callingAppInfo)
60
+ origin = systemOrigin
61
+ displayOrigin = systemOrigin
62
+
46
63
  val publicKeyRequest = request!!.callingRequest as CreatePublicKeyCredentialRequest
47
- val requestJson = JSONObject(publicKeyRequest.requestJson)
64
+ val rawJson = bundleRequestJson ?: publicKeyRequest.requestJson
65
+ val requestJson = JSONObject(rawJson)
66
+ val pkJson = if (requestJson.has("publicKey")) requestJson.getJSONObject("publicKey") else requestJson
48
67
 
49
68
  // Use RP ID as origin for derivation if available, otherwise fallback to app signature
50
- val rpId = requestJson.optJSONObject("rp")?.optString("id")
51
- origin = rpId ?: credentialRepository.getOrigin(request!!.callingAppInfo)
69
+ val rpId = pkJson.optJSONObject("rp")?.optString("id")
70
+ if (rpId != null && rpId.isNotEmpty()) {
71
+ origin = rpId
72
+ displayOrigin = rpId
73
+ }
52
74
 
53
- val userJson = requestJson.optJSONObject("user")
54
- userHandle = userJson?.optString("name") ?: "unknown-user"
55
- userName = userJson?.optString("displayName") ?: userHandle
56
- userId = userJson?.optString("id") ?: "unknown-id"
75
+ userHandle = pkJson.optJSONObject("user")?.optString("name") ?: "unknown-user"
76
+ userName = pkJson.optJSONObject("user")?.optString("displayName") ?: userHandle
77
+ userId = pkJson.optJSONObject("user")?.optString("id") ?: "unknown-id"
57
78
  } catch (e: Exception) {
58
79
  Log.e(TAG, "Error parsing request", e)
59
80
  }
60
81
  }
61
82
 
62
- // Simple UI for demonstration
63
- val layout = android.widget.LinearLayout(this).apply {
64
- orientation = android.widget.LinearLayout.VERTICAL
65
- setPadding(50, 50, 50, 50)
83
+ // Improved UI
84
+ val layout = LinearLayout(this).apply {
85
+ val padding = (32 * resources.displayMetrics.density).toInt()
86
+ orientation = LinearLayout.VERTICAL
87
+ setPadding(padding, padding, padding, padding)
88
+ gravity = Gravity.CENTER_HORIZONTAL
89
+ setBackgroundColor(Color.WHITE)
66
90
  }
67
91
 
92
+ // Header with App Icon and Provider Label
93
+ try {
94
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
95
+ val appIcon = packageManager.getApplicationIcon(appInfo)
96
+ val appLabel = packageManager.getApplicationLabel(appInfo)
97
+
98
+ val density = resources.displayMetrics.density
99
+ val iconSize = (48 * density).toInt()
100
+ val iconMargin = (12 * density).toInt()
101
+ val headerPadding = (40 * density).toInt()
102
+
103
+ val header = LinearLayout(this).apply {
104
+ orientation = LinearLayout.HORIZONTAL
105
+ gravity = Gravity.CENTER_VERTICAL
106
+ setPadding(0, 0, 0, headerPadding)
107
+ }
108
+
109
+ val iconView = ImageView(this).apply {
110
+ setImageDrawable(appIcon)
111
+ layoutParams = LinearLayout.LayoutParams(iconSize, iconSize).apply {
112
+ marginEnd = iconMargin
113
+ }
114
+ }
115
+ header.addView(iconView)
116
+
117
+ val providerLabel = TextView(this).apply {
118
+ text = appLabel
119
+ textSize = 16f
120
+ typeface = Typeface.DEFAULT_BOLD
121
+ setTextColor(Color.DKGRAY)
122
+ }
123
+ header.addView(providerLabel)
124
+ layout.addView(header)
125
+ } catch (e: Exception) {
126
+ // Fallback if app info cannot be loaded
127
+ }
128
+
68
129
  val title = TextView(this).apply {
69
- text = "Create New Passkey"
70
- textSize = 24f
71
- setPadding(0, 0, 0, 50)
130
+ text = "Create Passkey"
131
+ textSize = 28f
132
+ typeface = Typeface.DEFAULT_BOLD
133
+ setPadding(0, 0, 0, (8 * resources.displayMetrics.density).toInt())
134
+ setTextColor(Color.BLACK)
72
135
  }
73
136
  layout.addView(title)
74
137
 
75
- val info = TextView(this).apply {
76
- text = "For: $userName\nAt: $origin"
77
- setPadding(0, 0, 0, 50)
138
+ val description = TextView(this).apply {
139
+ text = "Create a new passkey for $displayOrigin"
140
+ textSize = 16f
141
+ setPadding(0, 0, 0, (40 * resources.displayMetrics.density).toInt())
142
+ gravity = Gravity.CENTER_HORIZONTAL
143
+ setTextColor(Color.GRAY)
144
+ }
145
+ layout.addView(description)
146
+
147
+ val userInfoContainer = LinearLayout(this).apply {
148
+ orientation = LinearLayout.VERTICAL
149
+ setPadding(0, 0, 0, (50 * resources.displayMetrics.density).toInt())
150
+ gravity = Gravity.CENTER_HORIZONTAL
151
+ }
152
+
153
+ val userLabel = TextView(this).apply {
154
+ text = "ACCOUNT"
155
+ textSize = 12f
156
+ typeface = Typeface.DEFAULT_BOLD
157
+ setTextColor(Color.GRAY)
158
+ setPadding(0, 0, 0, (4 * resources.displayMetrics.density).toInt())
159
+ }
160
+ userInfoContainer.addView(userLabel)
161
+
162
+ val userValue = TextView(this).apply {
163
+ text = userName
164
+ textSize = 20f
165
+ typeface = Typeface.DEFAULT_BOLD
166
+ setTextColor(Color.BLACK)
78
167
  }
79
- layout.addView(info)
168
+ userInfoContainer.addView(userValue)
169
+
170
+ if (userHandle != userName) {
171
+ val handleValue = TextView(this).apply {
172
+ text = userHandle
173
+ textSize = 14f
174
+ setTextColor(Color.GRAY)
175
+ }
176
+ userInfoContainer.addView(handleValue)
177
+ }
178
+ layout.addView(userInfoContainer)
80
179
 
81
180
  val confirmButton = Button(this).apply {
82
181
  text = "Create Passkey"
83
182
  setOnClickListener {
84
183
  handleCreation()
85
184
  }
185
+ layoutParams = LinearLayout.LayoutParams(
186
+ LinearLayout.LayoutParams.MATCH_PARENT,
187
+ LinearLayout.LayoutParams.WRAP_CONTENT
188
+ ).apply {
189
+ setMargins(0, 0, 0, (8 * resources.displayMetrics.density).toInt())
190
+ }
86
191
  }
87
192
  layout.addView(confirmButton)
88
193
 
194
+ val cancelButton = Button(this, null, android.R.attr.borderlessButtonStyle).apply {
195
+ text = "Cancel"
196
+ setOnClickListener {
197
+ setResult(RESULT_CANCELED)
198
+ finish()
199
+ }
200
+ layoutParams = LinearLayout.LayoutParams(
201
+ LinearLayout.LayoutParams.MATCH_PARENT,
202
+ LinearLayout.LayoutParams.WRAP_CONTENT
203
+ )
204
+ }
205
+ layout.addView(cancelButton)
206
+
89
207
  setContentView(layout)
90
208
  }
91
209
 
@@ -93,8 +211,25 @@ class CreatePasskeyActivity : AppCompatActivity() {
93
211
  private fun handleCreation() {
94
212
  try {
95
213
  val req = request ?: throw IllegalStateException("No request found")
96
- val publicKeyRequest = req.callingRequest as CreatePublicKeyCredentialRequest
97
- val requestOptions = PublicKeyCredentialCreationOptions(publicKeyRequest.requestJson)
214
+
215
+ // Prefer using the request JSON from the bundle if available
216
+ val rawRequestJson = bundleRequestJson ?: run {
217
+ val publicKeyRequest = req.callingRequest as CreatePublicKeyCredentialRequest
218
+ publicKeyRequest.requestJson
219
+ }
220
+
221
+ val requestJson = JSONObject(rawRequestJson)
222
+ val passkeyReqJson = if (requestJson.has("publicKey")) {
223
+ requestJson.getJSONObject("publicKey").toString()
224
+ } else {
225
+ rawRequestJson
226
+ }
227
+ val requestOptions = try {
228
+ PublicKeyCredentialCreationOptions(passkeyReqJson)
229
+ } catch (e: org.json.JSONException) {
230
+ Log.e(TAG, "Invalid passkey creation request JSON: $passkeyReqJson")
231
+ throw e
232
+ }
98
233
 
99
234
  val keyPair: KeyPair = credentialRepository.createDeterministicKeyPair(this, origin, userHandle)
100
235
  val credentialId = credentialRepository.generateCredentialId(keyPair)
@@ -6,7 +6,12 @@ import android.content.Intent
6
6
  import android.os.Build
7
7
  import android.os.Bundle
8
8
  import android.util.Log
9
+ import android.graphics.Color
10
+ import android.graphics.Typeface
11
+ import android.view.Gravity
9
12
  import android.widget.Button
13
+ import android.widget.ImageView
14
+ import android.widget.LinearLayout
10
15
  import android.widget.TextView
11
16
  import androidx.annotation.RequiresApi
12
17
  import androidx.appcompat.app.AppCompatActivity
@@ -34,8 +39,10 @@ class GetPasskeyActivity : AppCompatActivity() {
34
39
  }
35
40
 
36
41
  private var origin: String = "unknown-origin"
42
+ private var displayOrigin: String = "unknown-origin"
37
43
  private var userHandle: String = "unknown-user"
38
44
  private var credentialIdEnc: String? = null
45
+ private var bundleRequestJson: String? = null
39
46
  private var request: ProviderGetCredentialRequest? = null
40
47
 
41
48
  override fun onCreate(savedInstanceState: Bundle?) {
@@ -44,39 +51,147 @@ class GetPasskeyActivity : AppCompatActivity() {
44
51
  request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
45
52
  val credentialData = intent.getBundleExtra("CREDENTIAL_DATA")
46
53
 
47
- if (request != null && credentialData != null) {
48
- origin = credentialRepository.getOrigin(request!!.callingAppInfo)
54
+ if (credentialData != null) {
49
55
  credentialIdEnc = credentialData.getString("credentialId")
50
56
  userHandle = credentialData.getString("userHandle") ?: "unknown-user"
57
+ bundleRequestJson = credentialData.getString("requestJson")
58
+ }
59
+
60
+ if (request != null) {
61
+ origin = credentialRepository.getOrigin(request!!.callingAppInfo)
62
+ displayOrigin = origin
63
+
64
+ // Try to extract rpId for better display
65
+ val rawJson = bundleRequestJson ?: (request?.credentialOptions?.get(0) as? GetPublicKeyCredentialOption)?.requestJson
66
+ if (rawJson != null) {
67
+ try {
68
+ val jsonObj = JSONObject(rawJson)
69
+ val pkJson = if (jsonObj.has("publicKey")) jsonObj.getJSONObject("publicKey") else jsonObj
70
+ val rpId = pkJson.optString("rpId")
71
+ if (rpId.isNotEmpty()) {
72
+ displayOrigin = rpId
73
+ }
74
+ } catch (e: Exception) {
75
+ // ignore
76
+ }
77
+ }
51
78
  }
52
79
 
53
- // Simple UI
54
- val layout = android.widget.LinearLayout(this).apply {
55
- orientation = android.widget.LinearLayout.VERTICAL
56
- setPadding(50, 50, 50, 50)
80
+ // Improved UI
81
+ val layout = LinearLayout(this).apply {
82
+ val padding = (32 * resources.displayMetrics.density).toInt()
83
+ orientation = LinearLayout.VERTICAL
84
+ setPadding(padding, padding, padding, padding)
85
+ gravity = Gravity.CENTER_HORIZONTAL
86
+ setBackgroundColor(Color.WHITE)
87
+ }
88
+
89
+ // Header with App Icon and Provider Label
90
+ try {
91
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
92
+ val appIcon = packageManager.getApplicationIcon(appInfo)
93
+ val appLabel = packageManager.getApplicationLabel(appInfo)
94
+
95
+ val density = resources.displayMetrics.density
96
+ val iconSize = (48 * density).toInt()
97
+ val iconMargin = (12 * density).toInt()
98
+ val headerPadding = (40 * density).toInt()
99
+
100
+ val header = LinearLayout(this).apply {
101
+ orientation = LinearLayout.HORIZONTAL
102
+ gravity = Gravity.CENTER_VERTICAL
103
+ setPadding(0, 0, 0, headerPadding)
104
+ }
105
+
106
+ val iconView = ImageView(this).apply {
107
+ setImageDrawable(appIcon)
108
+ layoutParams = LinearLayout.LayoutParams(iconSize, iconSize).apply {
109
+ marginEnd = iconMargin
110
+ }
111
+ }
112
+ header.addView(iconView)
113
+
114
+ val providerLabel = TextView(this).apply {
115
+ text = appLabel
116
+ textSize = 16f
117
+ typeface = Typeface.DEFAULT_BOLD
118
+ setTextColor(Color.DKGRAY)
119
+ }
120
+ header.addView(providerLabel)
121
+ layout.addView(header)
122
+ } catch (e: Exception) {
123
+ // Fallback if app info cannot be loaded
57
124
  }
58
125
 
59
126
  val title = TextView(this).apply {
60
127
  text = "Use Passkey"
61
- textSize = 24f
62
- setPadding(0, 0, 0, 50)
128
+ textSize = 28f
129
+ typeface = Typeface.DEFAULT_BOLD
130
+ setPadding(0, 0, 0, (8 * resources.displayMetrics.density).toInt())
131
+ setTextColor(Color.BLACK)
63
132
  }
64
133
  layout.addView(title)
65
134
 
66
- val info = TextView(this).apply {
67
- text = "User: $userHandle\nAt: $origin"
68
- setPadding(0, 0, 0, 50)
135
+ val description = TextView(this).apply {
136
+ text = "Sign in to $displayOrigin"
137
+ textSize = 16f
138
+ setPadding(0, 0, 0, (40 * resources.displayMetrics.density).toInt())
139
+ gravity = Gravity.CENTER_HORIZONTAL
140
+ setTextColor(Color.GRAY)
141
+ }
142
+ layout.addView(description)
143
+
144
+ val userInfoContainer = LinearLayout(this).apply {
145
+ orientation = LinearLayout.VERTICAL
146
+ setPadding(0, 0, 0, (50 * resources.displayMetrics.density).toInt())
147
+ gravity = Gravity.CENTER_HORIZONTAL
69
148
  }
70
- layout.addView(info)
149
+
150
+ val userLabel = TextView(this).apply {
151
+ text = "ACCOUNT"
152
+ textSize = 12f
153
+ typeface = Typeface.DEFAULT_BOLD
154
+ setTextColor(Color.GRAY)
155
+ setPadding(0, 0, 0, (4 * resources.displayMetrics.density).toInt())
156
+ }
157
+ userInfoContainer.addView(userLabel)
158
+
159
+ val userValue = TextView(this).apply {
160
+ text = userHandle
161
+ textSize = 20f
162
+ typeface = Typeface.DEFAULT_BOLD
163
+ setTextColor(Color.BLACK)
164
+ }
165
+ userInfoContainer.addView(userValue)
166
+ layout.addView(userInfoContainer)
71
167
 
72
168
  val confirmButton = Button(this).apply {
73
169
  text = "Sign In"
74
170
  setOnClickListener {
75
171
  handleAssertion()
76
172
  }
173
+ layoutParams = LinearLayout.LayoutParams(
174
+ LinearLayout.LayoutParams.MATCH_PARENT,
175
+ LinearLayout.LayoutParams.WRAP_CONTENT
176
+ ).apply {
177
+ setMargins(0, 0, 0, (8 * resources.displayMetrics.density).toInt())
178
+ }
77
179
  }
78
180
  layout.addView(confirmButton)
79
181
 
182
+ val cancelButton = Button(this, null, android.R.attr.borderlessButtonStyle).apply {
183
+ text = "Cancel"
184
+ setOnClickListener {
185
+ setResult(RESULT_CANCELED)
186
+ finish()
187
+ }
188
+ layoutParams = LinearLayout.LayoutParams(
189
+ LinearLayout.LayoutParams.MATCH_PARENT,
190
+ LinearLayout.LayoutParams.WRAP_CONTENT
191
+ )
192
+ }
193
+ layout.addView(cancelButton)
194
+
80
195
  setContentView(layout)
81
196
  }
82
197
 
@@ -84,19 +199,47 @@ class GetPasskeyActivity : AppCompatActivity() {
84
199
  private fun handleAssertion() {
85
200
  try {
86
201
  val req = request ?: throw IllegalStateException("No request found")
87
- val option = req.credentialOptions[0] as GetPublicKeyCredentialOption
88
- val requestOptions = PublicKeyCredentialRequestOptions(option.requestJson)
202
+
203
+ // Prefer using the request JSON from the bundle if available, as it's specifically for this entry
204
+ val rawRequestJson = bundleRequestJson ?: run {
205
+ val option = req.credentialOptions[0] as GetPublicKeyCredentialOption
206
+ option.requestJson
207
+ }
208
+
209
+ val requestJson = JSONObject(rawRequestJson)
210
+ val passkeyReqJson = if (requestJson.has("publicKey")) {
211
+ requestJson.getJSONObject("publicKey").toString()
212
+ } else {
213
+ rawRequestJson
214
+ }
215
+ val requestOptions = try {
216
+ PublicKeyCredentialRequestOptions(passkeyReqJson)
217
+ } catch (e: org.json.JSONException) {
218
+ Log.e(TAG, "Invalid passkey request JSON: $passkeyReqJson")
219
+ throw e
220
+ }
89
221
 
90
222
  val credId = AndroidBase64.decode(credentialIdEnc!!, AndroidBase64.DEFAULT)
91
223
 
92
- val requestJson = JSONObject(option.requestJson)
93
- val challenge = requestJson.getString("challenge")
224
+ val passkeyRequestJsonObj = JSONObject(passkeyReqJson)
225
+ val challenge = if (passkeyRequestJsonObj.has("challenge")) {
226
+ passkeyRequestJsonObj.getString("challenge")
227
+ } else {
228
+ throw org.json.JSONException("No value for challenge in requestJson: $passkeyReqJson")
229
+ }
94
230
  val sanitizedOrigin = origin.replace(Regex("/$"), "")
95
231
 
96
- val clientDataHash = option.requestData.getByteArray("androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH")
97
- ?: MessageDigest.getInstance("SHA-256").digest(
98
- "{\"type\":\"webauthn.get\",\"challenge\":\"$challenge\",\"origin\":\"$sanitizedOrigin\",\"crossOrigin\":false}".toByteArray(Charsets.UTF_8)
99
- )
232
+ val clientDataJSON = JSONObject()
233
+ clientDataJSON.put("type", "webauthn.get")
234
+ clientDataJSON.put("challenge", challenge)
235
+ clientDataJSON.put("origin", sanitizedOrigin)
236
+ clientDataJSON.put("crossOrigin", false)
237
+ if (sanitizedOrigin.startsWith("android:apk-key-hash:")) {
238
+ clientDataJSON.put("androidPackageName", req.callingAppInfo.packageName)
239
+ }
240
+
241
+ val clientDataJSONString = clientDataJSON.toString()
242
+ val clientDataHash = MessageDigest.getInstance("SHA-256").digest(clientDataJSONString.toByteArray(Charsets.UTF_8))
100
243
 
101
244
  val dbCred = credentialRepository.getCredential(this, credId)
102
245
  ?: throw IllegalStateException("Credential not found")
@@ -104,7 +247,7 @@ class GetPasskeyActivity : AppCompatActivity() {
104
247
  val response = AuthenticatorAssertionResponse(
105
248
  requestOptions = requestOptions,
106
249
  credentialId = credId,
107
- origin = credentialRepository.getOrigin(req.callingAppInfo),
250
+ origin = sanitizedOrigin,
108
251
  up = true,
109
252
  uv = true,
110
253
  be = true,
@@ -125,13 +268,23 @@ class GetPasskeyActivity : AppCompatActivity() {
125
268
  authenticatorAttachment = "platform"
126
269
  )
127
270
 
128
- // Manual addition of clientDataJSON if needed by some RPs, following reference
129
- val clientDataJSONb64 = getClientDataJSONb64(origin, challenge)
271
+ // Manual addition of clientDataJSON and signature to the response JSON
272
+ val clientDataJSONb64 = AndroidBase64.encodeToString(clientDataJSONString.toByteArray(), AndroidBase64.URL_SAFE or AndroidBase64.NO_WRAP or AndroidBase64.NO_PADDING)
273
+ val signatureb64 = AndroidBase64.encodeToString(response.signature, AndroidBase64.URL_SAFE or AndroidBase64.NO_WRAP or AndroidBase64.NO_PADDING)
274
+
275
+ val fullJson = JSONObject(fidoCredential.json())
276
+ val respJson = fullJson.getJSONObject("response")
277
+ respJson.put("clientDataJSON", clientDataJSONb64)
278
+ respJson.put("signature", signatureb64)
130
279
 
131
- val delimiter = "response\":{"
132
- val credentialJson = fidoCredential.json().substringBeforeLast(delimiter) + delimiter +
133
- "\"clientDataJSON\":\"$clientDataJSONb64\"," +
134
- fidoCredential.json().substringAfterLast(delimiter)
280
+ // Add clientExtensionResults as seen in the example
281
+ val clientExtensionResults = JSONObject()
282
+ val credProps = JSONObject()
283
+ credProps.put("rk", true)
284
+ clientExtensionResults.put("credProps", credProps)
285
+ fullJson.put("clientExtensionResults", clientExtensionResults)
286
+
287
+ val credentialJson = fullJson.toString()
135
288
 
136
289
  val resultIntent = Intent()
137
290
  val passkeyCredential = PublicKeyCredential(credentialJson)
@@ -149,9 +302,4 @@ class GetPasskeyActivity : AppCompatActivity() {
149
302
  finish()
150
303
  }
151
304
 
152
- private fun getClientDataJSONb64(origin: String, challenge: String): String {
153
- val sanitizedOrigin = origin.replace(Regex("/$"), "")
154
- val jsonString = "{\"type\":\"webauthn.get\",\"challenge\":\"$challenge\",\"origin\":\"$sanitizedOrigin\",\"crossOrigin\":false}"
155
- return AndroidBase64.encodeToString(jsonString.toByteArray(), AndroidBase64.NO_WRAP)
156
- }
157
305
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algorandfoundation/react-native-passkey-autofill",
3
- "version": "1.0.0-canary.3",
3
+ "version": "1.0.0-canary.4",
4
4
  "description": "Passkey AutoFill React Native",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",