@@ -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
596637private fun PrivateKey.deriveKeyFromKeyPair (): SecretKeySpec {
0 commit comments