Skip to content

Commit ab37846

Browse files
committed
fix: LocalContainer not adhereing to specifications
Use correct encoding and creation of clientDataJson
1 parent e6e51be commit ab37846

File tree

1 file changed

+115
-74
lines changed

1 file changed

+115
-74
lines changed

wrapper/src/main/java/io/yubicolabs/wwwwallet/credentials/LocalContainer.kt

Lines changed: 115 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -70,45 +70,38 @@ class LocalContainer(
7070
options: JSONObject,
7171
successCallback: (JSONObject) -> Unit,
7272
failureCallback: (Throwable) -> Unit,
73+
) = create(options, null, successCallback, failureCallback)
74+
75+
fun create(
76+
options: JSONObject,
77+
clientDataJsonHash: ByteArray?,
78+
successCallback: (JSONObject) -> Unit,
79+
failureCallback: (Throwable) -> Unit,
7380
) = try {
74-
val credential = createCredential(options)
81+
val credential = createCredential(options, clientDataJsonHash)
7582
successCallback(credential)
7683
} catch (th: Throwable) {
7784
YOLOLogger.e(tagForLog, "Cannot create credential.", th)
7885
failureCallback(th)
7986
}
8087

81-
private fun createCredential(options: JSONObject): JSONObject {
88+
private fun createCredential(
89+
options: JSONObject,
90+
clientDataHash: ByteArray?,
91+
): JSONObject {
8292
val selectedAlgorithm = selectAlgorithm(options)
8393
val (algorithmSpec, keyAlgorithm) = getAlgorithmParams(selectedAlgorithm)
8494

8595
val credentialId = ByteArray(32)
8696
SecureRandom().nextBytes(credentialId)
8797

88-
val challenge =
89-
(options.getNested("publicKey.challenge") as? String)?.decodeBase64()?.toByteArray()
90-
?: byteArrayOf()
91-
92-
val spec = buildKeyGenParameterSpec(credentialId, challenge, algorithmSpec)
98+
val spec = buildKeyGenParameterSpec(credentialId, algorithmSpec)
9399
val keyPair = generateKeyPair(keyAlgorithm, spec)
94100

95101
val rpId =
96102
options.getNested("publicKey.rp.id") as? String
97103
?: throw IllegalStateException("'publicKey.rp.id' on credential create options not set.")
98104

99-
val clientDataJson =
100-
getClientOptions(
101-
type = "webauthn.create",
102-
challenge = challenge,
103-
origin = rpId,
104-
)
105-
106-
val clientDataJsonB64 =
107-
encodeToString(
108-
clientDataJson,
109-
NO_PADDING or NO_WRAP or URL_SAFE,
110-
)
111-
112105
val (attestationObject, authenticatorData) =
113106
createAttestationObject(
114107
rpId = rpId,
@@ -120,10 +113,14 @@ class LocalContainer(
120113
signatureCount = 0,
121114
)
122115

116+
val challenge =
117+
(options.getNested("publicKey.challenge") as? String)?.decodeBase64()?.toByteArray()
118+
?: throw (IllegalStateException("Challenge not present."))
119+
123120
val credential =
124121
createPublicKeyCredential(
125122
credentialId,
126-
clientDataJsonB64,
123+
clientDataHash ?: getClientOptions(type = "webauthn.create", challenge = challenge, origin = rpId),
127124
attestationObject,
128125
authenticatorData,
129126
selectedAlgorithm,
@@ -137,6 +134,35 @@ class LocalContainer(
137134
return credential
138135
}
139136

137+
fun delete(
138+
credentialId: String,
139+
successCallback: () -> Unit,
140+
failureCallback: (Throwable) -> Unit,
141+
) = try {
142+
val byteId = credentialId.decodeBase64()?.toByteArray() ?: byteArrayOf()
143+
val alias = "$origin+${byteId.toHexString()}"
144+
val filename = byteId.credIdToFilename()
145+
146+
if (secureStore.containsAlias(alias)) {
147+
secureStore.deleteEntry(alias)
148+
} else {
149+
failureCallback(IllegalStateException("Credential alias nor found."))
150+
}
151+
152+
if (filename !in context.fileList()) {
153+
// no delete necessary
154+
failureCallback(IllegalStateException("File $filename not found."))
155+
} else {
156+
if (context.deleteFile(filename)) {
157+
successCallback()
158+
} else {
159+
failureCallback(RuntimeException("Failed to delete $filename file."))
160+
}
161+
}
162+
} catch (th: Throwable) {
163+
failureCallback(th)
164+
}
165+
140166
private fun selectAlgorithm(options: JSONObject): Int {
141167
val pubKeyCredParams: List<*> =
142168
options.getNested("publicKey.pubKeyCredParams") as? List<*> ?: listOf<Any>()
@@ -157,23 +183,19 @@ class LocalContainer(
157183

158184
private fun buildKeyGenParameterSpec(
159185
credentialId: ByteArray,
160-
challenge: ByteArray,
161186
algorithmSpec: AlgorithmParameterSpec,
162187
): KeyGenParameterSpec =
163188
KeyGenParameterSpec
164189
.Builder(
165190
"$origin+${credentialId.toHexString()}",
166191
PURPOSE_SIGN or PURPOSE_VERIFY,
167-
)
168-
.setAlgorithmParameterSpec(algorithmSpec)
192+
).setAlgorithmParameterSpec(algorithmSpec)
169193
.setIsStrongBoxBacked(
170194
isStrongBoxed,
171195
).setDigests(
172196
DIGEST_SHA256,
173197
DIGEST_SHA384,
174198
DIGEST_SHA512,
175-
).setAttestationChallenge(
176-
challenge,
177199
).build()
178200

179201
private fun generateKeyPair(
@@ -191,7 +213,7 @@ class LocalContainer(
191213

192214
private fun createPublicKeyCredential(
193215
credentialId: ByteArray,
194-
clientDataJsonB64: String,
216+
clientDataHash: ByteArray,
195217
attestationObject: String,
196218
authenticatorData: ByteArray,
197219
publicKeyAlgorithm: Int,
@@ -200,7 +222,11 @@ class LocalContainer(
200222
val response =
201223
JSONObject(
202224
mapOf(
203-
"clientDataJSON" to clientDataJsonB64,
225+
"clientDataJSON" to
226+
encodeToString(
227+
clientDataHash,
228+
NO_PADDING or NO_WRAP or URL_SAFE,
229+
),
204230
"attestationObject" to attestationObject,
205231
"authenticatorData" to
206232
encodeToString(
@@ -241,9 +267,13 @@ class LocalContainer(
241267
type: String,
242268
challenge: ByteArray,
243269
origin: String,
244-
): ByteArray {
245-
return """{"type":"$type","challenge":"${encodeToString(challenge, NO_PADDING or NO_WRAP or URL_SAFE)}","origin":"${origin.fullyQualified()}","crossOrigin":false}""".toByteArray()
246-
}
270+
): ByteArray =
271+
"""{"type":"$type","challenge":"${
272+
encodeToString(
273+
challenge,
274+
NO_PADDING or NO_WRAP or URL_SAFE,
275+
)
276+
}","origin":"${origin.fullyQualified()}","crossOrigin":false}""".toByteArray()
247277

248278
private fun createAttestationObject(
249279
rpId: String,
@@ -266,7 +296,8 @@ class LocalContainer(
266296

267297
val attestationObject =
268298
encodeToString(
269-
CBORObject.NewMap()
299+
CBORObject
300+
.NewMap()
270301
.Add("fmt", "none")
271302
.Add("attStmt", CBORObject.NewMap())
272303
.Add("authData", authenticatorData)
@@ -281,7 +312,8 @@ class LocalContainer(
281312
cosePublicKey: ByteArray,
282313
): ByteArray {
283314
val attestedCredentialDataLength = 16 + 2 + credentialId.size + cosePublicKey.size
284-
return ByteBuffer.allocate(attestedCredentialDataLength)
315+
return ByteBuffer
316+
.allocate(attestedCredentialDataLength)
285317
.order(ByteOrder.BIG_ENDIAN)
286318
.put(aaguid.toByteArray(), 0, 16)
287319
.putShort(credentialId.size.toShort())
@@ -309,7 +341,8 @@ class LocalContainer(
309341
?: 0
310342
) + (extensions?.size ?: 0)
311343
val authenticatorData =
312-
ByteBuffer.allocate(authenticatorDataLength)
344+
ByteBuffer
345+
.allocate(authenticatorDataLength)
313346
.order(ByteOrder.BIG_ENDIAN)
314347
.put(rpIdHash, 0, 32)
315348
.put(flags)
@@ -345,53 +378,57 @@ class LocalContainer(
345378
return flags.toByte()
346379
}
347380

348-
private fun isAlgorithmSupported(alg: Int): Boolean {
349-
return alg in listOf(-7, -257, -35, -36)
350-
}
381+
private fun isAlgorithmSupported(alg: Int): Boolean = alg in listOf(-7, -257, -35, -36)
351382

352-
private fun getAlgorithmParams(alg: Int): Pair<AlgorithmParameterSpec, String> {
353-
return when (alg) {
383+
private fun getAlgorithmParams(alg: Int): Pair<AlgorithmParameterSpec, String> =
384+
when (alg) {
354385
-7 -> ECGenParameterSpec("secp256r1") to KEY_ALGORITHM_EC
355386
-35 -> ECGenParameterSpec("secp384r1") to KEY_ALGORITHM_EC
356387
-36 -> ECGenParameterSpec("secp521r1") to KEY_ALGORITHM_EC
357388
else -> throw IllegalArgumentException("Unsupported algorithm: $alg")
358389
}
359-
}
360390

361391
fun getAll(
362392
options: JSONObject,
393+
maybeClientDataJsonHash: ByteArray? = null,
363394
successCallback: (JSONArray) -> Unit,
364395
failureCallback: (Throwable) -> Unit,
365396
) = try {
397+
YOLOLogger.i("found credentials", "get options: ${options.toString(2)}")
398+
366399
val allowedCredentials: List<Map<*, *>> =
367400
(options.getNested("publicKey.allowCredentials") as? List<Map<*, *>>) ?: listOf<Map<*, *>>()
368401

402+
val rpId = options.getNested("publicKey.rpId") as? String ?: ""
403+
369404
val challenge =
370405
(options.getNested("publicKey.challenge") as? String)?.decodeBase64()?.toByteArray()
371406
?: byteArrayOf()
372407

373-
val rpId = options.getNested("publicKey.rpId") as? String ?: ""
408+
val clientDataJsonHash =
409+
maybeClientDataJsonHash ?: getClientOptions(type = "webauthn.get", challenge = challenge, origin = rpId)
374410

375411
// retrieve allowed or all credentials
376412
val selectedCredentials =
377413
if (allowedCredentials.isNotEmpty()) {
378-
allowedCredentials.mapNotNull { allowed ->
379-
val type = allowed.getOrDefault("type", null) as? String ?: ""
380-
if (type != "public-key") {
381-
YOLOLogger.e(tagForLog, "Found non 'public-key' credential id in allow list.")
382-
}
383-
384-
val allowedIdB64 = allowed.getOrDefault("id", null) as? String ?: ""
385-
val allowedIdRaw = allowedIdB64.decodeBase64()
386-
val allowedId = allowedIdRaw?.hex() ?: ""
387-
val alias = "$origin+$allowedId"
388-
389-
if (secureStore.containsAlias(alias)) {
390-
alias to secureStore.getEntry(alias, null)
391-
} else {
392-
null
393-
}
394-
}.associate { it }
414+
allowedCredentials
415+
.mapNotNull { allowed ->
416+
val type = allowed.getOrDefault("type", null) as? String ?: ""
417+
if (type != "public-key") {
418+
YOLOLogger.e(tagForLog, "Found non 'public-key' credential id in allow list.")
419+
}
420+
421+
val allowedIdB64 = allowed.getOrDefault("id", null) as? String ?: ""
422+
val allowedIdRaw = allowedIdB64.decodeBase64()
423+
val allowedId = allowedIdRaw?.hex() ?: ""
424+
val alias = "$origin+$allowedId"
425+
426+
if (secureStore.containsAlias(alias)) {
427+
alias to secureStore.getEntry(alias, null)
428+
} else {
429+
null
430+
}
431+
}.associate { it }
395432
} else {
396433
secureStore.aliases().toList().associate { key ->
397434
key to secureStore.getEntry(key, null)
@@ -408,11 +445,18 @@ class LocalContainer(
408445
finalSelection.mapNotNull { selectionEntry ->
409446
val (key, keyEntry) = selectionEntry
410447
(keyEntry as? KeyStore.PrivateKeyEntry)
411-
?.toResponse(key, challenge, rpId)
448+
?.toResponse(
449+
id = key,
450+
clientDataJson = clientDataJsonHash,
451+
rpId = rpId,
452+
)
412453
}
413454

455+
val credentialsJson = JSONArray(credentials)
456+
val msg = credentialsJson.toString(2)
457+
YOLOLogger.i("Found credentials", "get response: $msg")
414458
successCallback(
415-
JSONArray(credentials),
459+
credentialsJson,
416460
)
417461
} catch (th: Throwable) {
418462
YOLOLogger.e(tagForLog, "Couldn't return all credentials.", th)
@@ -423,9 +467,17 @@ class LocalContainer(
423467
options: JSONObject,
424468
successCallback: (JSONObject) -> Unit,
425469
failureCallback: (Throwable) -> Unit,
470+
) = get(options, null, successCallback, failureCallback)
471+
472+
fun get(
473+
options: JSONObject,
474+
clientDataJsonHash: ByteArray?,
475+
successCallback: (JSONObject) -> Unit,
476+
failureCallback: (Throwable) -> Unit,
426477
) = try {
427478
getAll(
428479
options = options,
480+
maybeClientDataJsonHash = clientDataJsonHash,
429481
successCallback = { jsonArray ->
430482
if (jsonArray.length() > 0) {
431483
successCallback(jsonArray.getJSONObject(0))
@@ -442,18 +494,12 @@ class LocalContainer(
442494

443495
private fun KeyStore.PrivateKeyEntry.toResponse(
444496
id: String,
445-
challenge: ByteArray,
497+
clientDataJson: ByteArray,
446498
rpId: String,
447499
): JSONObject {
448500
val credentialId =
449501
id.replace("$origin+", "").hexToByteArray()
450502

451-
val clientDataJson =
452-
getClientOptions(
453-
type = "webauthn.get",
454-
challenge = challenge,
455-
origin = rpId,
456-
)
457503
val clientDataJsonB64 =
458504
encodeToString(
459505
clientDataJson,
@@ -500,11 +546,7 @@ class LocalContainer(
500546
signature,
501547
NO_PADDING or NO_WRAP or URL_SAFE,
502548
),
503-
"userHandle" to
504-
encodeToString(
505-
userId.toByteArray(),
506-
NO_PADDING or NO_WRAP or URL_SAFE,
507-
),
549+
"userHandle" to userId,
508550
"userName" to userName,
509551
"userDisplayName" to userDisplayName,
510552
),
@@ -589,8 +631,7 @@ class LocalContainer(
589631
.digest(
590632
"AES+${toHexString()}"
591633
.toByteArray(),
592-
)
593-
.toHexString()
634+
).toHexString()
594635
}
595636

596637
private fun PrivateKey.deriveKeyFromKeyPair(): SecretKeySpec {

0 commit comments

Comments
 (0)