@authon/react-native-wallet 0.0.0-stage → 0.1.0
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 +221 -2
- package/android/build.gradle +40 -0
- package/android/consumer-rules.pro +10 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +181 -0
- package/android/src/vendor/GENERATED.md +4 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +191 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +82 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +232 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +353 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +58 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +449 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +71 -0
- package/build/authonContext.d.ts +19 -0
- package/build/authonContext.d.ts.map +1 -0
- package/build/authonContext.js +20 -0
- package/build/authonContext.js.map +1 -0
- package/build/client.d.ts +76 -0
- package/build/client.d.ts.map +1 -0
- package/build/client.js +349 -0
- package/build/client.js.map +1 -0
- package/build/errors.d.ts +18 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +9 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/native.d.ts +39 -0
- package/build/native.d.ts.map +1 -0
- package/build/native.js +12 -0
- package/build/native.js.map +1 -0
- package/build/types.d.ts +121 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +3 -0
- package/build/types.js.map +1 -0
- package/build/useAuthonWallet.d.ts +25 -0
- package/build/useAuthonWallet.d.ts.map +1 -0
- package/build/useAuthonWallet.js +77 -0
- package/build/useAuthonWallet.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AuthonReactNativeWallet.podspec +37 -0
- package/ios/AuthonWalletModule.swift +190 -0
- package/ios/vendor/AuthonWallet/AuthonWallet.swift +212 -0
- package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
- package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
- package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +104 -0
- package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
- package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
- package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
- package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
- package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
- package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
- package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
- package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
- package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
- package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
- package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
- package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
- package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
- package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
- package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +350 -0
- package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +555 -0
- package/ios/vendor/AuthonWallet/WalletError.swift +54 -0
- package/ios/vendor/CKeccak/LICENSE +22 -0
- package/ios/vendor/CKeccak/README.md +27 -0
- package/ios/vendor/CKeccak/byte_order.h +59 -0
- package/ios/vendor/CKeccak/include/ckeccak.h +9 -0
- package/ios/vendor/CKeccak/memzero.c +75 -0
- package/ios/vendor/CKeccak/memzero.h +8 -0
- package/ios/vendor/CKeccak/options.h +99 -0
- package/ios/vendor/CKeccak/sha3.c +413 -0
- package/ios/vendor/CKeccak/sha3.h +89 -0
- package/ios/vendor/CSecp256k1/COPYING +19 -0
- package/ios/vendor/CSecp256k1/README.md +35 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
- package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
- package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
- package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
- package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
- package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
- package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
- package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
- package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
- package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
- package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
- package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
- package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
- package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
- package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
- package/ios/vendor/CSecp256k1/src/field.h +350 -0
- package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
- package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
- package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
- package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
- package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
- package/ios/vendor/CSecp256k1/src/group.h +212 -0
- package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
- package/ios/vendor/CSecp256k1/src/hash.h +44 -0
- package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
- package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
- package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
- package/ios/vendor/CSecp256k1/src/int128.h +90 -0
- package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
- package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
- package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
- package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
- package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
- package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
- package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
- package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
- package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
- package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
- package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
- package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
- package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
- package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
- package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
- package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
- package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
- package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
- package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
- package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
- package/ios/vendor/CSecp256k1/src/util.h +451 -0
- package/ios/vendor/GENERATED.md +4 -0
- package/package.json +65 -3
- package/src/authonContext.ts +30 -0
- package/src/client.ts +375 -0
- package/src/errors.ts +52 -0
- package/src/index.ts +21 -0
- package/src/native.ts +50 -0
- package/src/types.ts +120 -0
- package/src/useAuthonWallet.ts +103 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Bytes.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import java.security.MessageDigest
|
|
5
|
+
import java.security.SecureRandom
|
|
6
|
+
import java.util.Base64
|
|
7
|
+
|
|
8
|
+
internal object Bytes {
|
|
9
|
+
private val random = SecureRandom()
|
|
10
|
+
|
|
11
|
+
fun utf8(value: String): ByteArray = value.toByteArray(Charsets.UTF_8)
|
|
12
|
+
|
|
13
|
+
fun toBase64(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
|
|
14
|
+
|
|
15
|
+
/** Accepts standard and URL-safe alphabets, with or without padding (like the frame's atob path). */
|
|
16
|
+
fun fromBase64(value: String): ByteArray {
|
|
17
|
+
val normalized = value.replace('-', '+').replace('_', '/').trimEnd('=')
|
|
18
|
+
return try {
|
|
19
|
+
Base64.getDecoder().decode(normalized.padEnd((normalized.length + 3) / 4 * 4, '='))
|
|
20
|
+
} catch (e: IllegalArgumentException) {
|
|
21
|
+
throw WalletException(WalletErrorCode.INVALID_REQUEST, "invalid base64", e)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun toBase64Url(bytes: ByteArray): String = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
|
|
26
|
+
|
|
27
|
+
fun toHex(bytes: ByteArray): String {
|
|
28
|
+
val out = StringBuilder(bytes.size * 2)
|
|
29
|
+
for (b in bytes) {
|
|
30
|
+
val v = b.toInt() and 0xff
|
|
31
|
+
out.append(HEX[v ushr 4]).append(HEX[v and 0x0f])
|
|
32
|
+
}
|
|
33
|
+
return out.toString()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Hex with or without 0x; null when malformed. */
|
|
37
|
+
fun fromHexOrNull(value: String): ByteArray? {
|
|
38
|
+
val body = if (value.startsWith("0x") || value.startsWith("0X")) value.substring(2) else value
|
|
39
|
+
if (body.length % 2 != 0) return null
|
|
40
|
+
val out = ByteArray(body.length / 2)
|
|
41
|
+
for (i in out.indices) {
|
|
42
|
+
val hi = Character.digit(body[i * 2], 16)
|
|
43
|
+
val lo = Character.digit(body[i * 2 + 1], 16)
|
|
44
|
+
if (hi < 0 || lo < 0) return null
|
|
45
|
+
out[i] = ((hi shl 4) or lo).toByte()
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun fromHex(value: String): ByteArray =
|
|
51
|
+
fromHexOrNull(value) ?: throw WalletException(WalletErrorCode.INVALID_REQUEST, "invalid hex")
|
|
52
|
+
|
|
53
|
+
fun concat(vararg parts: ByteArray): ByteArray {
|
|
54
|
+
val out = ByteArray(parts.sumOf { it.size })
|
|
55
|
+
var offset = 0
|
|
56
|
+
for (p in parts) {
|
|
57
|
+
p.copyInto(out, offset)
|
|
58
|
+
offset += p.size
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fun randomBytes(length: Int): ByteArray = ByteArray(length).also { random.nextBytes(it) }
|
|
64
|
+
|
|
65
|
+
fun wipe(vararg buffers: ByteArray?) {
|
|
66
|
+
for (b in buffers) b?.fill(0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun sha256(data: ByteArray): ByteArray = MessageDigest.getInstance("SHA-256").digest(data)
|
|
70
|
+
|
|
71
|
+
fun sameBytes(a: ByteArray, b: ByteArray): Boolean = MessageDigest.isEqual(a, b)
|
|
72
|
+
|
|
73
|
+
private const val HEX = "0123456789abcdef"
|
|
74
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import androidx.credentials.CreatePublicKeyCredentialRequest
|
|
6
|
+
import androidx.credentials.CreatePublicKeyCredentialResponse
|
|
7
|
+
import androidx.credentials.CredentialManager
|
|
8
|
+
import androidx.credentials.GetCredentialRequest
|
|
9
|
+
import androidx.credentials.GetPublicKeyCredentialOption
|
|
10
|
+
import androidx.credentials.PublicKeyCredential
|
|
11
|
+
import androidx.credentials.exceptions.CreateCredentialCancellationException
|
|
12
|
+
import androidx.credentials.exceptions.CreateCredentialException
|
|
13
|
+
import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException
|
|
14
|
+
import androidx.credentials.exceptions.CreateCredentialUnsupportedException
|
|
15
|
+
import androidx.credentials.exceptions.GetCredentialCancellationException
|
|
16
|
+
import androidx.credentials.exceptions.GetCredentialException
|
|
17
|
+
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
|
|
18
|
+
import androidx.credentials.exceptions.GetCredentialUnsupportedException
|
|
19
|
+
import androidx.credentials.exceptions.NoCredentialException
|
|
20
|
+
import androidx.credentials.exceptions.domerrors.DomError
|
|
21
|
+
import androidx.credentials.exceptions.domerrors.NotAllowedError
|
|
22
|
+
import androidx.credentials.exceptions.domerrors.NotSupportedError
|
|
23
|
+
import androidx.credentials.exceptions.domerrors.SecurityError
|
|
24
|
+
import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException
|
|
25
|
+
import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException
|
|
26
|
+
import kotlinx.coroutines.Dispatchers
|
|
27
|
+
import kotlinx.coroutines.withContext
|
|
28
|
+
import kotlinx.serialization.json.Json
|
|
29
|
+
import kotlinx.serialization.json.JsonObject
|
|
30
|
+
import kotlinx.serialization.json.JsonPrimitive
|
|
31
|
+
import kotlinx.serialization.json.addJsonObject
|
|
32
|
+
import kotlinx.serialization.json.booleanOrNull
|
|
33
|
+
import kotlinx.serialization.json.buildJsonObject
|
|
34
|
+
import kotlinx.serialization.json.contentOrNull
|
|
35
|
+
import kotlinx.serialization.json.intOrNull
|
|
36
|
+
import kotlinx.serialization.json.put
|
|
37
|
+
import kotlinx.serialization.json.putJsonArray
|
|
38
|
+
import kotlinx.serialization.json.putJsonObject
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Backup passkeys through Credential Manager with WebAuthn JSON for RP `wallet.authon.dev`
|
|
42
|
+
* (the same RP as the web frame, so synced passkeys restore on either side). PRF is requested with
|
|
43
|
+
* `extensions.prf.eval.first` and read from `clientExtensionResults.prf.results.first`.
|
|
44
|
+
*
|
|
45
|
+
* The app must be listed in the RP's Digital Asset Links (`get_login_creds`), which the API
|
|
46
|
+
* publishes only for approved app registrations.
|
|
47
|
+
*/
|
|
48
|
+
class CredentialManagerPasskeys(
|
|
49
|
+
private val activityProvider: () -> Activity?,
|
|
50
|
+
private val rpId: String = WalletApi.DEFAULT_RP_ID,
|
|
51
|
+
) : PasskeyProvider {
|
|
52
|
+
|
|
53
|
+
private fun activity(): Activity = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed }
|
|
54
|
+
?: throw walletError(WalletErrorCode.INTERNAL, "no foreground activity for the passkey prompt")
|
|
55
|
+
|
|
56
|
+
override suspend fun create(userId: ByteArray, userName: String, salt: ByteArray): PasskeyCreateResult {
|
|
57
|
+
val request = buildJsonObject {
|
|
58
|
+
put("challenge", Bytes.toBase64Url(Bytes.randomBytes(32)))
|
|
59
|
+
putJsonObject("rp") {
|
|
60
|
+
put("id", rpId)
|
|
61
|
+
put("name", "Authon Wallet")
|
|
62
|
+
}
|
|
63
|
+
putJsonObject("user") {
|
|
64
|
+
put("id", Bytes.toBase64Url(userId))
|
|
65
|
+
put("name", userName)
|
|
66
|
+
put("displayName", userName)
|
|
67
|
+
}
|
|
68
|
+
putJsonArray("pubKeyCredParams") {
|
|
69
|
+
for (alg in listOf(-7, -257, -8)) addJsonObject {
|
|
70
|
+
put("type", "public-key")
|
|
71
|
+
put("alg", alg)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
put("attestation", "none")
|
|
75
|
+
putJsonObject("authenticatorSelection") {
|
|
76
|
+
put("residentKey", "preferred")
|
|
77
|
+
put("userVerification", "required")
|
|
78
|
+
}
|
|
79
|
+
prfExtension(salt)
|
|
80
|
+
}
|
|
81
|
+
val activity = activity()
|
|
82
|
+
val response = try {
|
|
83
|
+
withContext(Dispatchers.Main) {
|
|
84
|
+
CredentialManager.create(activity).createCredential(activity, CreatePublicKeyCredentialRequest(request.toString()))
|
|
85
|
+
}
|
|
86
|
+
} catch (e: CreateCredentialException) {
|
|
87
|
+
throw mapCreate(e)
|
|
88
|
+
}
|
|
89
|
+
val json = (response as? CreatePublicKeyCredentialResponse)?.registrationResponseJson
|
|
90
|
+
?: throw walletError(WalletErrorCode.INTERNAL, "unexpected credential response")
|
|
91
|
+
val cred = Json.parseToJsonElement(json) as JsonObject
|
|
92
|
+
val ext = cred["clientExtensionResults"] as? JsonObject
|
|
93
|
+
val prf = ext?.get("prf") as? JsonObject
|
|
94
|
+
if (prf == null || (prf["enabled"] as? JsonPrimitive)?.booleanOrNull == false) throw walletError(WalletErrorCode.PRF_UNSUPPORTED)
|
|
95
|
+
val credentialId = normalize(cred.str("rawId") ?: cred.str("id") ?: throw walletError(WalletErrorCode.INTERNAL, "credential id missing"))
|
|
96
|
+
val resp = cred["response"] as? JsonObject ?: JsonObject(emptyMap())
|
|
97
|
+
val alg = (resp["publicKeyAlgorithm"] as? JsonPrimitive)?.intOrNull
|
|
98
|
+
val spki = resp.str("publicKey")?.takeIf { alg == -7 || alg == -257 || alg == -8 }?.let { normalize(it) }
|
|
99
|
+
val attestation = resp.str("attestationObject")?.let { normalize(it) }
|
|
100
|
+
val direct = prfFirst(prf)
|
|
101
|
+
val prfOutput = direct ?: get(credentialId, salt, null).prfOutput
|
|
102
|
+
return PasskeyCreateResult(credentialId, prfOutput, spki, if (spki != null) alg else null, attestation)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override suspend fun get(credentialId: String, salt: ByteArray, challenge: ByteArray?): PasskeyGetResult {
|
|
106
|
+
val request = buildJsonObject {
|
|
107
|
+
put("challenge", Bytes.toBase64Url(challenge ?: Bytes.randomBytes(32)))
|
|
108
|
+
put("rpId", rpId)
|
|
109
|
+
putJsonArray("allowCredentials") {
|
|
110
|
+
addJsonObject {
|
|
111
|
+
put("type", "public-key")
|
|
112
|
+
put("id", normalize(credentialId))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
put("userVerification", "required")
|
|
116
|
+
prfExtension(salt)
|
|
117
|
+
}
|
|
118
|
+
val activity = activity()
|
|
119
|
+
val result = try {
|
|
120
|
+
withContext(Dispatchers.Main) {
|
|
121
|
+
CredentialManager.create(activity).getCredential(
|
|
122
|
+
activity,
|
|
123
|
+
GetCredentialRequest(listOf(GetPublicKeyCredentialOption(request.toString()))),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
} catch (e: GetCredentialException) {
|
|
127
|
+
throw mapGet(e)
|
|
128
|
+
}
|
|
129
|
+
val json = (result.credential as? PublicKeyCredential)?.authenticationResponseJson
|
|
130
|
+
?: throw walletError(WalletErrorCode.INTERNAL, "unexpected credential response")
|
|
131
|
+
val cred = Json.parseToJsonElement(json) as JsonObject
|
|
132
|
+
val prf = (cred["clientExtensionResults"] as? JsonObject)?.get("prf") as? JsonObject
|
|
133
|
+
val out = prf?.let { prfFirst(it) } ?: throw walletError(WalletErrorCode.PRF_UNSUPPORTED)
|
|
134
|
+
val resp = cred["response"] as? JsonObject ?: JsonObject(emptyMap())
|
|
135
|
+
fun field(name: String) = resp.str(name)?.let { normalize(it) } ?: ""
|
|
136
|
+
return PasskeyGetResult(
|
|
137
|
+
out,
|
|
138
|
+
PasskeyAssertion(
|
|
139
|
+
credentialId = normalize(cred.str("rawId") ?: cred.str("id") ?: credentialId),
|
|
140
|
+
clientDataJSON = field("clientDataJSON"),
|
|
141
|
+
authenticatorData = field("authenticatorData"),
|
|
142
|
+
signature = field("signature"),
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun kotlinx.serialization.json.JsonObjectBuilder.prfExtension(salt: ByteArray) {
|
|
148
|
+
putJsonObject("extensions") {
|
|
149
|
+
putJsonObject("prf") {
|
|
150
|
+
putJsonObject("eval") { put("first", Bytes.toBase64Url(salt)) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private fun prfFirst(prf: JsonObject): ByteArray? {
|
|
156
|
+
val first = ((prf["results"] as? JsonObject)?.get("first") as? JsonPrimitive)?.contentOrNull ?: return null
|
|
157
|
+
val bytes = runCatching { Bytes.fromBase64(first) }.getOrNull() ?: return null
|
|
158
|
+
return bytes.takeIf { it.size == 32 }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun normalize(b64: String): String = Bytes.toBase64Url(Bytes.fromBase64(b64))
|
|
162
|
+
|
|
163
|
+
private fun JsonObject.str(k: String) = (this[k] as? JsonPrimitive)?.takeIf { it.isString }?.content
|
|
164
|
+
|
|
165
|
+
private fun mapDom(error: DomError, cause: Exception): WalletException = when (error) {
|
|
166
|
+
is NotAllowedError -> WalletException(WalletErrorCode.USER_CANCELLED, cause.message, cause)
|
|
167
|
+
is SecurityError -> WalletException(
|
|
168
|
+
WalletErrorCode.ORIGIN_NOT_ALLOWED,
|
|
169
|
+
"this app is not linked to $rpId (Digital Asset Links): ${cause.message}",
|
|
170
|
+
cause,
|
|
171
|
+
)
|
|
172
|
+
is NotSupportedError -> WalletException(WalletErrorCode.PRF_UNSUPPORTED, cause.message, cause)
|
|
173
|
+
else -> WalletException(WalletErrorCode.INTERNAL, "passkey failure", cause)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private fun mapCreate(e: CreateCredentialException): WalletException = when (e) {
|
|
177
|
+
is CreateCredentialCancellationException -> WalletException(WalletErrorCode.USER_CANCELLED, e.message, e)
|
|
178
|
+
is CreateCredentialUnsupportedException, is CreateCredentialProviderConfigurationException ->
|
|
179
|
+
WalletException(WalletErrorCode.PRF_UNSUPPORTED, e.message, e)
|
|
180
|
+
is CreatePublicKeyCredentialDomException -> mapDom(e.domError, e)
|
|
181
|
+
else -> WalletException(WalletErrorCode.INTERNAL, "passkey failure", e)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun mapGet(e: GetCredentialException): WalletException = when (e) {
|
|
185
|
+
is GetCredentialCancellationException -> WalletException(WalletErrorCode.USER_CANCELLED, e.message, e)
|
|
186
|
+
is NoCredentialException -> WalletException(WalletErrorCode.USER_CANCELLED, "no matching backup passkey", e)
|
|
187
|
+
is GetCredentialUnsupportedException, is GetCredentialProviderConfigurationException ->
|
|
188
|
+
WalletException(WalletErrorCode.PRF_UNSUPPORTED, e.message, e)
|
|
189
|
+
is GetPublicKeyCredentialDomException -> mapDom(e.domError, e)
|
|
190
|
+
else -> WalletException(WalletErrorCode.INTERNAL, "passkey failure", e)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/DeviceStore.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import java.math.BigInteger
|
|
5
|
+
import java.security.KeyPairGenerator
|
|
6
|
+
import java.security.PrivateKey
|
|
7
|
+
import java.security.Signature
|
|
8
|
+
import java.security.interfaces.ECPublicKey
|
|
9
|
+
import java.security.spec.ECGenParameterSpec
|
|
10
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
11
|
+
import javax.crypto.Cipher
|
|
12
|
+
import javax.crypto.KeyGenerator
|
|
13
|
+
import javax.crypto.SecretKey
|
|
14
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
15
|
+
|
|
16
|
+
class SealedShare(val iv: ByteArray, val ciphertext: ByteArray)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The non-exportable keys of one device record: a P-256 signing key (its public JWK is registered
|
|
20
|
+
* with the API) and an AES-256-GCM key that seals the device share D.
|
|
21
|
+
*/
|
|
22
|
+
interface DeviceKeys {
|
|
23
|
+
/** Identifies the keys inside their store (e.g. the Keystore alias prefix). */
|
|
24
|
+
val handle: String
|
|
25
|
+
val publicJwk: DevicePublicJwk
|
|
26
|
+
|
|
27
|
+
/** ECDSA P-256 / SHA-256 over [data]; raw r||s (64 bytes) like WebCrypto. */
|
|
28
|
+
fun sign(data: ByteArray): ByteArray
|
|
29
|
+
|
|
30
|
+
fun seal(plain: ByteArray, aad: ByteArray): SealedShare
|
|
31
|
+
|
|
32
|
+
fun open(sealed: SealedShare, aad: ByteArray): ByteArray
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data class DeviceRecord(
|
|
36
|
+
/** `projectId \0 userId`. */
|
|
37
|
+
val key: String,
|
|
38
|
+
val keys: DeviceKeys,
|
|
39
|
+
val iv: ByteArray,
|
|
40
|
+
val ciphertext: ByteArray,
|
|
41
|
+
val deviceId: String,
|
|
42
|
+
val walletId: String,
|
|
43
|
+
val address: String,
|
|
44
|
+
val shareVersion: Int,
|
|
45
|
+
val createdAt: Long,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
/** Where the device share lives. Only D (sealed) and key references are stored; never k, A or B. */
|
|
49
|
+
interface DeviceStore {
|
|
50
|
+
suspend fun generateKeys(): DeviceKeys
|
|
51
|
+
|
|
52
|
+
/** Removes keys that never made it into a stored record. */
|
|
53
|
+
suspend fun discardKeys(keys: DeviceKeys)
|
|
54
|
+
|
|
55
|
+
suspend fun get(key: String): DeviceRecord?
|
|
56
|
+
|
|
57
|
+
/** Replaces the record for [DeviceRecord.key]; keys of a replaced record are removed. */
|
|
58
|
+
suspend fun put(record: DeviceRecord)
|
|
59
|
+
|
|
60
|
+
suspend fun delete(key: String)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
internal object DeviceStorage {
|
|
64
|
+
fun storageKey(projectId: String, userId: String): String = "$projectId\u0000$userId"
|
|
65
|
+
|
|
66
|
+
/** DER ECDSA signature -> raw r||s, each left-padded to [size] bytes. */
|
|
67
|
+
fun derToRaw(der: ByteArray, size: Int = 32): ByteArray {
|
|
68
|
+
var i = 0
|
|
69
|
+
fun readLen(): Int {
|
|
70
|
+
var len = der[i++].toInt() and 0xff
|
|
71
|
+
if (len and 0x80 != 0) {
|
|
72
|
+
val n = len and 0x7f
|
|
73
|
+
len = 0
|
|
74
|
+
repeat(n) { len = (len shl 8) or (der[i++].toInt() and 0xff) }
|
|
75
|
+
}
|
|
76
|
+
return len
|
|
77
|
+
}
|
|
78
|
+
require(der[i++].toInt() == 0x30) { "not a DER sequence" }
|
|
79
|
+
readLen()
|
|
80
|
+
fun readInt(): ByteArray {
|
|
81
|
+
require(der[i++].toInt() == 0x02) { "not a DER integer" }
|
|
82
|
+
val len = readLen()
|
|
83
|
+
val v = der.copyOfRange(i, i + len)
|
|
84
|
+
i += len
|
|
85
|
+
return v
|
|
86
|
+
}
|
|
87
|
+
val r = readInt()
|
|
88
|
+
val s = readInt()
|
|
89
|
+
val out = ByteArray(size * 2)
|
|
90
|
+
fun place(v: ByteArray, offset: Int) {
|
|
91
|
+
val trimmed = v.dropWhile { it.toInt() == 0 }.toByteArray()
|
|
92
|
+
require(trimmed.size <= size) { "integer too long" }
|
|
93
|
+
trimmed.copyInto(out, offset + size - trimmed.size)
|
|
94
|
+
}
|
|
95
|
+
place(r, 0)
|
|
96
|
+
place(s, size)
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun unsigned32(v: BigInteger): ByteArray {
|
|
101
|
+
val raw = v.toByteArray()
|
|
102
|
+
val trimmed = if (raw.size > 32) raw.copyOfRange(raw.size - 32, raw.size) else raw
|
|
103
|
+
return ByteArray(32 - trimmed.size) + trimmed
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun jwkOf(key: ECPublicKey): DevicePublicJwk =
|
|
107
|
+
DevicePublicJwk(Bytes.toBase64Url(unsigned32(key.w.affineX)), Bytes.toBase64Url(unsigned32(key.w.affineY)))
|
|
108
|
+
|
|
109
|
+
fun gcmSeal(key: SecretKey, plain: ByteArray, aad: ByteArray, iv: ByteArray? = null): SealedShare {
|
|
110
|
+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
111
|
+
if (iv == null) cipher.init(Cipher.ENCRYPT_MODE, key) else cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(128, iv))
|
|
112
|
+
cipher.updateAAD(aad)
|
|
113
|
+
val ct = cipher.doFinal(plain)
|
|
114
|
+
return SealedShare(cipher.iv, ct)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fun gcmOpen(key: SecretKey, sealed: SealedShare, aad: ByteArray): ByteArray {
|
|
118
|
+
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
119
|
+
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, sealed.iv))
|
|
120
|
+
cipher.updateAAD(aad)
|
|
121
|
+
return cipher.doFinal(sealed.ciphertext)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* In-memory store with software JCA keys. For JVM tests and emulators only: nothing survives the
|
|
127
|
+
* process and the keys are not hardware-backed.
|
|
128
|
+
*/
|
|
129
|
+
class InMemoryDeviceStore : DeviceStore {
|
|
130
|
+
private val records = ConcurrentHashMap<String, DeviceRecord>()
|
|
131
|
+
private var counter = 0
|
|
132
|
+
|
|
133
|
+
private class SoftwareKeys(override val handle: String) : DeviceKeys {
|
|
134
|
+
private val signKey: PrivateKey
|
|
135
|
+
private val aesKey: SecretKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
|
136
|
+
override val publicJwk: DevicePublicJwk
|
|
137
|
+
|
|
138
|
+
init {
|
|
139
|
+
val pair = KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }.generateKeyPair()
|
|
140
|
+
signKey = pair.private
|
|
141
|
+
publicJwk = DeviceStorage.jwkOf(pair.public as ECPublicKey)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun sign(data: ByteArray): ByteArray {
|
|
145
|
+
val s = Signature.getInstance("SHA256withECDSA")
|
|
146
|
+
s.initSign(signKey)
|
|
147
|
+
s.update(data)
|
|
148
|
+
return DeviceStorage.derToRaw(s.sign())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override fun seal(plain: ByteArray, aad: ByteArray): SealedShare =
|
|
152
|
+
DeviceStorage.gcmSeal(aesKey, plain, aad, Bytes.randomBytes(12))
|
|
153
|
+
|
|
154
|
+
override fun open(sealed: SealedShare, aad: ByteArray): ByteArray = DeviceStorage.gcmOpen(aesKey, sealed, aad)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override suspend fun generateKeys(): DeviceKeys = synchronized(this) { SoftwareKeys("mem-${++counter}") }
|
|
158
|
+
|
|
159
|
+
override suspend fun discardKeys(keys: DeviceKeys) = Unit
|
|
160
|
+
|
|
161
|
+
override suspend fun get(key: String): DeviceRecord? = records[key]
|
|
162
|
+
|
|
163
|
+
override suspend fun put(record: DeviceRecord) {
|
|
164
|
+
records[record.key] = record
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override suspend fun delete(key: String) {
|
|
168
|
+
records.remove(key)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt. Do not edit: change the source and run the script.
|
|
2
|
+
package dev.authon.sdk.wallet
|
|
3
|
+
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.app.AlertDialog
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.Typeface
|
|
9
|
+
import android.hardware.biometrics.BiometricManager
|
|
10
|
+
import android.hardware.biometrics.BiometricPrompt
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.os.CancellationSignal
|
|
13
|
+
import android.util.TypedValue
|
|
14
|
+
import android.view.ViewGroup
|
|
15
|
+
import android.widget.LinearLayout
|
|
16
|
+
import android.widget.ScrollView
|
|
17
|
+
import android.widget.TextView
|
|
18
|
+
import kotlinx.coroutines.Dispatchers
|
|
19
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
20
|
+
import kotlinx.coroutines.withContext
|
|
21
|
+
import kotlin.coroutines.resume
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default approval UI: a platform AlertDialog with the summary (rows, warnings, the full text).
|
|
25
|
+
* "Sign" is enabled only after the text was scrolled to the end, like the web approval card.
|
|
26
|
+
*/
|
|
27
|
+
class DialogWalletApprover(private val activityProvider: () -> Activity?) : WalletApprover {
|
|
28
|
+
|
|
29
|
+
override suspend fun approve(summary: SignSummary): Boolean = withContext(Dispatchers.Main) {
|
|
30
|
+
val activity = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed }
|
|
31
|
+
?: throw walletError(WalletErrorCode.CONFIRM_WINDOW_BLOCKED, "no foreground activity for the approval")
|
|
32
|
+
suspendCancellableCoroutine { cont ->
|
|
33
|
+
val scroll = ScrollView(activity)
|
|
34
|
+
scroll.addView(content(activity, summary))
|
|
35
|
+
var answered = false
|
|
36
|
+
val dialog = AlertDialog.Builder(activity)
|
|
37
|
+
.setTitle(summary.title)
|
|
38
|
+
.setView(scroll)
|
|
39
|
+
.setPositiveButton("Sign") { _, _ ->
|
|
40
|
+
answered = true
|
|
41
|
+
if (cont.isActive) cont.resume(true)
|
|
42
|
+
}
|
|
43
|
+
.setNegativeButton("Cancel") { _, _ ->
|
|
44
|
+
answered = true
|
|
45
|
+
if (cont.isActive) cont.resume(false)
|
|
46
|
+
}
|
|
47
|
+
.setOnDismissListener {
|
|
48
|
+
if (!answered && cont.isActive) cont.resume(false)
|
|
49
|
+
}
|
|
50
|
+
.create()
|
|
51
|
+
dialog.setCanceledOnTouchOutside(false)
|
|
52
|
+
dialog.setOnShowListener {
|
|
53
|
+
val sign = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
|
54
|
+
fun update() {
|
|
55
|
+
sign.isEnabled = !scroll.canScrollVertically(1)
|
|
56
|
+
}
|
|
57
|
+
update()
|
|
58
|
+
scroll.setOnScrollChangeListener { _, _, _, _, _ -> if (!sign.isEnabled) update() }
|
|
59
|
+
scroll.post { update() }
|
|
60
|
+
}
|
|
61
|
+
cont.invokeOnCancellation { dialog.dismiss() }
|
|
62
|
+
dialog.show()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun content(context: Context, summary: SignSummary): LinearLayout {
|
|
67
|
+
val pad = dp(context, 20)
|
|
68
|
+
val layout = LinearLayout(context).apply {
|
|
69
|
+
orientation = LinearLayout.VERTICAL
|
|
70
|
+
setPadding(pad, dp(context, 8), pad, dp(context, 8))
|
|
71
|
+
}
|
|
72
|
+
for (w in summary.warnings) {
|
|
73
|
+
layout.addView(text(context, w).apply { setTextColor(Color.rgb(0xb4, 0x23, 0x18)); setPadding(0, 0, 0, dp(context, 8)) })
|
|
74
|
+
}
|
|
75
|
+
for (row in summary.rows) {
|
|
76
|
+
layout.addView(text(context, row.label).apply { alpha = 0.7f; setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) })
|
|
77
|
+
layout.addView(
|
|
78
|
+
text(context, row.value).apply {
|
|
79
|
+
if (row.mono) typeface = Typeface.MONOSPACE
|
|
80
|
+
setTextIsSelectable(true)
|
|
81
|
+
setPadding(0, 0, 0, dp(context, 8))
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
summary.body?.let { body ->
|
|
86
|
+
layout.addView(
|
|
87
|
+
text(context, body).apply {
|
|
88
|
+
typeface = Typeface.MONOSPACE
|
|
89
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
|
90
|
+
setTextIsSelectable(true)
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
return layout
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun text(context: Context, value: String) = TextView(context).apply {
|
|
98
|
+
text = value
|
|
99
|
+
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun dp(context: Context, v: Int) = (v * context.resources.displayMetrics.density).toInt()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Device lock check with the platform BiometricPrompt: biometrics or the device credential on
|
|
107
|
+
* Android 10+, biometrics only on Android 9.
|
|
108
|
+
*/
|
|
109
|
+
class BiometricDeviceAuthenticator(private val activityProvider: () -> Activity?) : DeviceAuthenticator {
|
|
110
|
+
|
|
111
|
+
override suspend fun authenticate(reason: String): Boolean = withContext(Dispatchers.Main) {
|
|
112
|
+
val activity = activityProvider()?.takeUnless { it.isFinishing || it.isDestroyed }
|
|
113
|
+
?: throw walletError(WalletErrorCode.CONFIRM_WINDOW_BLOCKED, "no foreground activity for device authentication")
|
|
114
|
+
suspendCancellableCoroutine { cont ->
|
|
115
|
+
val executor = activity.mainExecutor
|
|
116
|
+
val builder = BiometricPrompt.Builder(activity).setTitle(reason).setSubtitle("Confirm it is you")
|
|
117
|
+
when {
|
|
118
|
+
Build.VERSION.SDK_INT >= 30 -> builder.setAllowedAuthenticators(
|
|
119
|
+
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL,
|
|
120
|
+
)
|
|
121
|
+
Build.VERSION.SDK_INT == 29 -> @Suppress("DEPRECATION") builder.setDeviceCredentialAllowed(true)
|
|
122
|
+
else -> builder.setNegativeButton("Cancel", executor) { _, _ -> if (cont.isActive) cont.resume(false) }
|
|
123
|
+
}
|
|
124
|
+
val signal = CancellationSignal()
|
|
125
|
+
cont.invokeOnCancellation { signal.cancel() }
|
|
126
|
+
builder.build().authenticate(
|
|
127
|
+
signal,
|
|
128
|
+
executor,
|
|
129
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
130
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
131
|
+
if (cont.isActive) cont.resume(true)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
135
|
+
if (cont.isActive) cont.resume(false)
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|