@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.2 → 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
@@ -21,6 +26,7 @@ import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions
21
26
  import co.algorand.passkeyautofill.credentials.CredentialRepository
22
27
  import co.algorand.passkeyautofill.credentials.Credential
23
28
  import java.security.KeyPair
29
+ import java.security.MessageDigest
24
30
  import android.util.Base64 as AndroidBase64
25
31
  import org.json.JSONObject
26
32
 
@@ -33,49 +39,159 @@ class GetPasskeyActivity : AppCompatActivity() {
33
39
  }
34
40
 
35
41
  private var origin: String = "unknown-origin"
42
+ private var displayOrigin: String = "unknown-origin"
36
43
  private var userHandle: String = "unknown-user"
37
44
  private var credentialIdEnc: String? = null
45
+ private var bundleRequestJson: String? = null
38
46
  private var request: ProviderGetCredentialRequest? = null
39
47
 
40
48
  override fun onCreate(savedInstanceState: Bundle?) {
41
49
  super.onCreate(savedInstanceState)
42
-
50
+
43
51
  request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
44
52
  val credentialData = intent.getBundleExtra("CREDENTIAL_DATA")
45
-
46
- if (request != null && credentialData != null) {
47
- origin = credentialRepository.getOrigin(request!!.callingAppInfo)
53
+
54
+ if (credentialData != null) {
48
55
  credentialIdEnc = credentialData.getString("credentialId")
49
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
+ }
50
78
  }
51
79
 
52
- // Simple UI
53
- val layout = android.widget.LinearLayout(this).apply {
54
- orientation = android.widget.LinearLayout.VERTICAL
55
- 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)
56
87
  }
57
-
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
124
+ }
125
+
58
126
  val title = TextView(this).apply {
59
127
  text = "Use Passkey"
60
- textSize = 24f
61
- 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)
62
132
  }
63
133
  layout.addView(title)
64
134
 
65
- val info = TextView(this).apply {
66
- text = "User: $userHandle\nAt: $origin"
67
- 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)
68
141
  }
69
- layout.addView(info)
70
-
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
148
+ }
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)
167
+
71
168
  val confirmButton = Button(this).apply {
72
169
  text = "Sign In"
73
170
  setOnClickListener {
74
171
  handleAssertion()
75
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
+ }
76
179
  }
77
180
  layout.addView(confirmButton)
78
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
+
79
195
  setContentView(layout)
80
196
  }
81
197
 
@@ -83,20 +199,55 @@ class GetPasskeyActivity : AppCompatActivity() {
83
199
  private fun handleAssertion() {
84
200
  try {
85
201
  val req = request ?: throw IllegalStateException("No request found")
86
- val option = req.credentialOptions[0] as GetPublicKeyCredentialOption
87
- val requestOptions = PublicKeyCredentialRequestOptions(option.requestJson)
88
202
 
89
- val credId = AndroidBase64.decode(credentialIdEnc!!, AndroidBase64.DEFAULT)
90
- val clientDataHash = option.requestData.getByteArray("androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH")
91
- ?: throw IllegalStateException("Missing clientDataHash")
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
+ }
92
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
+ }
221
+
222
+ val credId = AndroidBase64.decode(credentialIdEnc!!, AndroidBase64.DEFAULT)
223
+
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
+ }
230
+ val sanitizedOrigin = origin.replace(Regex("/$"), "")
231
+
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))
243
+
93
244
  val dbCred = credentialRepository.getCredential(this, credId)
94
245
  ?: throw IllegalStateException("Credential not found")
95
246
 
96
247
  val response = AuthenticatorAssertionResponse(
97
248
  requestOptions = requestOptions,
98
249
  credentialId = credId,
99
- origin = credentialRepository.getOrigin(req.callingAppInfo),
250
+ origin = sanitizedOrigin,
100
251
  up = true,
101
252
  uv = true,
102
253
  be = true,
@@ -117,24 +268,32 @@ class GetPasskeyActivity : AppCompatActivity() {
117
268
  authenticatorAttachment = "platform"
118
269
  )
119
270
 
120
- // Manual addition of clientDataJSON if needed by some RPs, following reference
121
- val requestJson = JSONObject(option.requestJson)
122
- val challenge = requestJson.getString("challenge")
123
- val clientDataJSONb64 = getClientDataJSONb64(origin, challenge)
124
-
125
- val delimiter = "response\":{"
126
- val credentialJson = fidoCredential.json().substringBeforeLast(delimiter) + delimiter +
127
- "\"clientDataJSON\":\"$clientDataJSONb64\"," +
128
- fidoCredential.json().substringAfterLast(delimiter)
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)
279
+
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()
129
288
 
130
289
  val resultIntent = Intent()
131
290
  val passkeyCredential = PublicKeyCredential(credentialJson)
132
-
291
+
133
292
  PendingIntentHandler.setGetCredentialResponse(
134
293
  resultIntent,
135
294
  GetCredentialResponse(passkeyCredential)
136
295
  )
137
-
296
+
138
297
  setResult(Activity.RESULT_OK, resultIntent)
139
298
  } catch (e: Exception) {
140
299
  Log.e(TAG, "Error during passkey assertion", e)
@@ -143,9 +302,4 @@ class GetPasskeyActivity : AppCompatActivity() {
143
302
  finish()
144
303
  }
145
304
 
146
- private fun getClientDataJSONb64(origin: String, challenge: String): String {
147
- val sanitizedOrigin = origin.replace(Regex("/$"), "")
148
- val jsonString = "{\"type\":\"webauthn.get\",\"challenge\":\"$challenge\",\"origin\":\"$sanitizedOrigin\",\"crossOrigin\":false}"
149
- return AndroidBase64.encodeToString(jsonString.toByteArray(), AndroidBase64.NO_WRAP)
150
- }
151
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.2",
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",