@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
|
-
#
|
|
1
|
+
# @algorandfoundation/react-native-passkey-autofill
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
51
|
-
|
|
69
|
+
val rpId = pkJson.optJSONObject("rp")?.optString("id")
|
|
70
|
+
if (rpId != null && rpId.isNotEmpty()) {
|
|
71
|
+
origin = rpId
|
|
72
|
+
displayOrigin = rpId
|
|
73
|
+
}
|
|
52
74
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
//
|
|
63
|
-
val layout =
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
70
|
-
textSize =
|
|
71
|
-
|
|
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
|
|
76
|
-
text = "
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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 (
|
|
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
|
-
//
|
|
53
|
-
val layout =
|
|
54
|
-
|
|
55
|
-
|
|
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 =
|
|
61
|
-
|
|
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
|
|
66
|
-
text = "
|
|
67
|
-
|
|
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(
|
|
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
|
-
|
|
90
|
-
val
|
|
91
|
-
|
|
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 =
|
|
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
|
|
121
|
-
val
|
|
122
|
-
val
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
val
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
}
|