@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.
Files changed (159) hide show
  1. package/README.md +221 -2
  2. package/android/build.gradle +40 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/src/main/AndroidManifest.xml +4 -0
  5. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +181 -0
  6. package/android/src/vendor/GENERATED.md +4 -0
  7. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +191 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +82 -0
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +232 -0
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
  21. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
  22. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
  23. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
  24. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +353 -0
  25. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +58 -0
  26. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +449 -0
  27. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +71 -0
  28. package/build/authonContext.d.ts +19 -0
  29. package/build/authonContext.d.ts.map +1 -0
  30. package/build/authonContext.js +20 -0
  31. package/build/authonContext.js.map +1 -0
  32. package/build/client.d.ts +76 -0
  33. package/build/client.d.ts.map +1 -0
  34. package/build/client.js +349 -0
  35. package/build/client.js.map +1 -0
  36. package/build/errors.d.ts +18 -0
  37. package/build/errors.d.ts.map +1 -0
  38. package/build/errors.js +9 -0
  39. package/build/errors.js.map +1 -0
  40. package/build/index.d.ts +7 -0
  41. package/build/index.d.ts.map +1 -0
  42. package/build/index.js +4 -0
  43. package/build/index.js.map +1 -0
  44. package/build/native.d.ts +39 -0
  45. package/build/native.d.ts.map +1 -0
  46. package/build/native.js +12 -0
  47. package/build/native.js.map +1 -0
  48. package/build/types.d.ts +121 -0
  49. package/build/types.d.ts.map +1 -0
  50. package/build/types.js +3 -0
  51. package/build/types.js.map +1 -0
  52. package/build/useAuthonWallet.d.ts +25 -0
  53. package/build/useAuthonWallet.d.ts.map +1 -0
  54. package/build/useAuthonWallet.js +77 -0
  55. package/build/useAuthonWallet.js.map +1 -0
  56. package/expo-module.config.json +9 -0
  57. package/ios/AuthonReactNativeWallet.podspec +37 -0
  58. package/ios/AuthonWalletModule.swift +190 -0
  59. package/ios/vendor/AuthonWallet/AuthonWallet.swift +212 -0
  60. package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
  61. package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
  62. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +104 -0
  63. package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
  64. package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
  65. package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
  66. package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
  67. package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
  68. package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
  69. package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
  70. package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
  71. package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
  72. package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
  73. package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
  74. package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
  75. package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
  76. package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
  77. package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
  78. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +350 -0
  79. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +555 -0
  80. package/ios/vendor/AuthonWallet/WalletError.swift +54 -0
  81. package/ios/vendor/CKeccak/LICENSE +22 -0
  82. package/ios/vendor/CKeccak/README.md +27 -0
  83. package/ios/vendor/CKeccak/byte_order.h +59 -0
  84. package/ios/vendor/CKeccak/include/ckeccak.h +9 -0
  85. package/ios/vendor/CKeccak/memzero.c +75 -0
  86. package/ios/vendor/CKeccak/memzero.h +8 -0
  87. package/ios/vendor/CKeccak/options.h +99 -0
  88. package/ios/vendor/CKeccak/sha3.c +413 -0
  89. package/ios/vendor/CKeccak/sha3.h +89 -0
  90. package/ios/vendor/CSecp256k1/COPYING +19 -0
  91. package/ios/vendor/CSecp256k1/README.md +35 -0
  92. package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
  93. package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
  94. package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
  95. package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
  96. package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
  97. package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
  98. package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
  99. package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
  100. package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
  101. package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
  102. package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
  103. package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
  104. package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
  105. package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
  106. package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
  107. package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
  108. package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
  109. package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
  110. package/ios/vendor/CSecp256k1/src/field.h +350 -0
  111. package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
  112. package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
  113. package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
  114. package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
  115. package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
  116. package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
  117. package/ios/vendor/CSecp256k1/src/group.h +212 -0
  118. package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
  119. package/ios/vendor/CSecp256k1/src/hash.h +44 -0
  120. package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
  121. package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
  122. package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
  123. package/ios/vendor/CSecp256k1/src/int128.h +90 -0
  124. package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
  125. package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
  126. package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
  127. package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
  128. package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
  129. package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
  130. package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
  131. package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
  132. package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
  133. package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
  134. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
  135. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
  136. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
  137. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
  138. package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
  139. package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
  140. package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
  141. package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
  142. package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
  143. package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
  144. package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
  145. package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
  146. package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
  147. package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
  148. package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
  149. package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
  150. package/ios/vendor/CSecp256k1/src/util.h +451 -0
  151. package/ios/vendor/GENERATED.md +4 -0
  152. package/package.json +65 -3
  153. package/src/authonContext.ts +30 -0
  154. package/src/client.ts +375 -0
  155. package/src/errors.ts +52 -0
  156. package/src/index.ts +21 -0
  157. package/src/native.ts +50 -0
  158. package/src/types.ts +120 -0
  159. 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
+ }