@doko/react-native-yubikit 0.0.2

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 (144) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +126 -0
  3. package/RNYubikit.podspec +22 -0
  4. package/android/build.gradle +63 -0
  5. package/android/src/main/AndroidManifest.xml +9 -0
  6. package/android/src/main/java/com/yubikit/YubiKitManagerHolder.kt +330 -0
  7. package/android/src/main/java/com/yubikit/YubikitCoreModule.kt +142 -0
  8. package/android/src/main/java/com/yubikit/YubikitFidoModule.kt +418 -0
  9. package/android/src/main/java/com/yubikit/YubikitManagementModule.kt +132 -0
  10. package/android/src/main/java/com/yubikit/YubikitOathModule.kt +224 -0
  11. package/android/src/main/java/com/yubikit/YubikitOpenPgpModule.kt +317 -0
  12. package/android/src/main/java/com/yubikit/YubikitPackage.kt +44 -0
  13. package/android/src/main/java/com/yubikit/YubikitPivModule.kt +310 -0
  14. package/android/src/main/java/com/yubikit/YubikitSupportModule.kt +108 -0
  15. package/android/src/main/java/com/yubikit/YubikitYubiOtpModule.kt +248 -0
  16. package/android/src/main/java/com/yubikit/utils/YubikitUtils.kt +321 -0
  17. package/ios/YubikitCore.h +5 -0
  18. package/ios/YubikitCore.mm +101 -0
  19. package/ios/YubikitDeviceInfoHelper.h +13 -0
  20. package/ios/YubikitDeviceInfoHelper.mm +110 -0
  21. package/ios/YubikitFido.h +5 -0
  22. package/ios/YubikitFido.mm +373 -0
  23. package/ios/YubikitManagement.h +5 -0
  24. package/ios/YubikitManagement.mm +153 -0
  25. package/ios/YubikitManager.h +27 -0
  26. package/ios/YubikitManager.m +169 -0
  27. package/ios/YubikitOath.h +5 -0
  28. package/ios/YubikitOath.mm +583 -0
  29. package/ios/YubikitOpenPgp.h +5 -0
  30. package/ios/YubikitOpenPgp.mm +210 -0
  31. package/ios/YubikitPiv.h +5 -0
  32. package/ios/YubikitPiv.mm +813 -0
  33. package/ios/YubikitSupport.h +5 -0
  34. package/ios/YubikitSupport.mm +58 -0
  35. package/ios/YubikitYubiOtp.h +5 -0
  36. package/ios/YubikitYubiOtp.mm +120 -0
  37. package/lib/module/core.js +34 -0
  38. package/lib/module/core.js.map +1 -0
  39. package/lib/module/fido.js +40 -0
  40. package/lib/module/fido.js.map +1 -0
  41. package/lib/module/hooks/index.js +5 -0
  42. package/lib/module/hooks/index.js.map +1 -0
  43. package/lib/module/hooks/use-yubikey.js +12 -0
  44. package/lib/module/hooks/use-yubikey.js.map +1 -0
  45. package/lib/module/hooks/yubikey-provider.js +85 -0
  46. package/lib/module/hooks/yubikey-provider.js.map +1 -0
  47. package/lib/module/index.js +12 -0
  48. package/lib/module/index.js.map +1 -0
  49. package/lib/module/management.js +17 -0
  50. package/lib/module/management.js.map +1 -0
  51. package/lib/module/oath.js +53 -0
  52. package/lib/module/oath.js.map +1 -0
  53. package/lib/module/openpgp.js +89 -0
  54. package/lib/module/openpgp.js.map +1 -0
  55. package/lib/module/package.json +1 -0
  56. package/lib/module/piv.js +71 -0
  57. package/lib/module/piv.js.map +1 -0
  58. package/lib/module/specs/NativeYubikitCore.js +10 -0
  59. package/lib/module/specs/NativeYubikitCore.js.map +1 -0
  60. package/lib/module/specs/NativeYubikitFido.js +5 -0
  61. package/lib/module/specs/NativeYubikitFido.js.map +1 -0
  62. package/lib/module/specs/NativeYubikitManagement.js +5 -0
  63. package/lib/module/specs/NativeYubikitManagement.js.map +1 -0
  64. package/lib/module/specs/NativeYubikitOath.js +5 -0
  65. package/lib/module/specs/NativeYubikitOath.js.map +1 -0
  66. package/lib/module/specs/NativeYubikitOpenPgp.js +5 -0
  67. package/lib/module/specs/NativeYubikitOpenPgp.js.map +1 -0
  68. package/lib/module/specs/NativeYubikitPiv.js +5 -0
  69. package/lib/module/specs/NativeYubikitPiv.js.map +1 -0
  70. package/lib/module/specs/NativeYubikitSupport.js +5 -0
  71. package/lib/module/specs/NativeYubikitSupport.js.map +1 -0
  72. package/lib/module/specs/NativeYubikitYubiOtp.js +5 -0
  73. package/lib/module/specs/NativeYubikitYubiOtp.js.map +1 -0
  74. package/lib/module/support.js +11 -0
  75. package/lib/module/support.js.map +1 -0
  76. package/lib/module/types.js +2 -0
  77. package/lib/module/types.js.map +1 -0
  78. package/lib/module/yubiotp.js +32 -0
  79. package/lib/module/yubiotp.js.map +1 -0
  80. package/lib/typescript/package.json +1 -0
  81. package/lib/typescript/src/core.d.ts +14 -0
  82. package/lib/typescript/src/core.d.ts.map +1 -0
  83. package/lib/typescript/src/fido.d.ts +24 -0
  84. package/lib/typescript/src/fido.d.ts.map +1 -0
  85. package/lib/typescript/src/hooks/index.d.ts +3 -0
  86. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  87. package/lib/typescript/src/hooks/use-yubikey.d.ts +3 -0
  88. package/lib/typescript/src/hooks/use-yubikey.d.ts.map +1 -0
  89. package/lib/typescript/src/hooks/yubikey-provider.d.ts +23 -0
  90. package/lib/typescript/src/hooks/yubikey-provider.d.ts.map +1 -0
  91. package/lib/typescript/src/index.d.ts +10 -0
  92. package/lib/typescript/src/index.d.ts.map +1 -0
  93. package/lib/typescript/src/management.d.ts +8 -0
  94. package/lib/typescript/src/management.d.ts.map +1 -0
  95. package/lib/typescript/src/oath.d.ts +20 -0
  96. package/lib/typescript/src/oath.d.ts.map +1 -0
  97. package/lib/typescript/src/openpgp.d.ts +35 -0
  98. package/lib/typescript/src/openpgp.d.ts.map +1 -0
  99. package/lib/typescript/src/piv.d.ts +26 -0
  100. package/lib/typescript/src/piv.d.ts.map +1 -0
  101. package/lib/typescript/src/specs/NativeYubikitCore.d.ts +49 -0
  102. package/lib/typescript/src/specs/NativeYubikitCore.d.ts.map +1 -0
  103. package/lib/typescript/src/specs/NativeYubikitFido.d.ts +121 -0
  104. package/lib/typescript/src/specs/NativeYubikitFido.d.ts.map +1 -0
  105. package/lib/typescript/src/specs/NativeYubikitManagement.d.ts +53 -0
  106. package/lib/typescript/src/specs/NativeYubikitManagement.d.ts.map +1 -0
  107. package/lib/typescript/src/specs/NativeYubikitOath.d.ts +70 -0
  108. package/lib/typescript/src/specs/NativeYubikitOath.d.ts.map +1 -0
  109. package/lib/typescript/src/specs/NativeYubikitOpenPgp.d.ts +53 -0
  110. package/lib/typescript/src/specs/NativeYubikitOpenPgp.d.ts.map +1 -0
  111. package/lib/typescript/src/specs/NativeYubikitPiv.d.ts +47 -0
  112. package/lib/typescript/src/specs/NativeYubikitPiv.d.ts.map +1 -0
  113. package/lib/typescript/src/specs/NativeYubikitSupport.d.ts +49 -0
  114. package/lib/typescript/src/specs/NativeYubikitSupport.d.ts.map +1 -0
  115. package/lib/typescript/src/specs/NativeYubikitYubiOtp.d.ts +45 -0
  116. package/lib/typescript/src/specs/NativeYubikitYubiOtp.d.ts.map +1 -0
  117. package/lib/typescript/src/support.d.ts +6 -0
  118. package/lib/typescript/src/support.d.ts.map +1 -0
  119. package/lib/typescript/src/types.d.ts +293 -0
  120. package/lib/typescript/src/types.d.ts.map +1 -0
  121. package/lib/typescript/src/yubiotp.d.ts +13 -0
  122. package/lib/typescript/src/yubiotp.d.ts.map +1 -0
  123. package/package.json +181 -0
  124. package/src/core.ts +58 -0
  125. package/src/fido.ts +112 -0
  126. package/src/hooks/index.tsx +2 -0
  127. package/src/hooks/use-yubikey.tsx +10 -0
  128. package/src/hooks/yubikey-provider.tsx +146 -0
  129. package/src/index.tsx +10 -0
  130. package/src/management.ts +44 -0
  131. package/src/oath.ts +134 -0
  132. package/src/openpgp.ts +220 -0
  133. package/src/piv.ts +193 -0
  134. package/src/specs/NativeYubikitCore.ts +68 -0
  135. package/src/specs/NativeYubikitFido.ts +149 -0
  136. package/src/specs/NativeYubikitManagement.ts +78 -0
  137. package/src/specs/NativeYubikitOath.ts +110 -0
  138. package/src/specs/NativeYubikitOpenPgp.ts +148 -0
  139. package/src/specs/NativeYubikitPiv.ts +108 -0
  140. package/src/specs/NativeYubikitSupport.ts +61 -0
  141. package/src/specs/NativeYubikitYubiOtp.ts +82 -0
  142. package/src/support.ts +18 -0
  143. package/src/types.ts +404 -0
  144. package/src/yubiotp.ts +96 -0
@@ -0,0 +1,142 @@
1
+ package com.yubikit
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReadableMap
7
+ import com.facebook.react.bridge.WritableArray
8
+ import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
9
+ import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
10
+ import com.yubico.yubikit.android.transport.usb.UsbConfiguration
11
+ import com.yubico.yubikit.core.fido.FidoConnection
12
+ import com.yubico.yubikit.core.otp.OtpConnection
13
+ import com.yubico.yubikit.core.smartcard.SmartCardConnection
14
+ import com.yubikit.YubiKitManagerHolder.deviceToBundle
15
+ import com.yubikit.utils.YubikitUtils.base64Decode
16
+ import com.yubikit.utils.YubikitUtils.base64Encode
17
+ import com.yubikit.utils.YubikitUtils.toWritableMap
18
+ import kotlinx.coroutines.CoroutineScope
19
+ import kotlinx.coroutines.Dispatchers
20
+ import kotlinx.coroutines.launch
21
+ import kotlinx.coroutines.withContext
22
+ import java.io.IOException
23
+
24
+ @Suppress("unused")
25
+ class YubikitCoreModule(reactContext: ReactApplicationContext) :
26
+ NativeYubikitCoreSpec(reactContext) {
27
+
28
+ private val moduleScope = CoroutineScope(Dispatchers.IO)
29
+
30
+ init {
31
+ YubiKitManagerHolder.setEventHandler { params -> emitOnYubiKeyEvent(params) }
32
+ }
33
+
34
+ override fun getName(): String = NAME
35
+
36
+ override fun startUsbDiscovery(config: ReadableMap?) {
37
+ YubiKitManagerHolder.initialize(reactApplicationContext)
38
+ val usbConfig = UsbConfiguration()
39
+ config?.let {
40
+ if (it.hasKey("handlePermissions") && !it.isNull("handlePermissions")) {
41
+ usbConfig.handlePermissions(it.getBoolean("handlePermissions"))
42
+ }
43
+ }
44
+ YubiKitManagerHolder.setUsbConfiguration(usbConfig)
45
+ YubiKitManagerHolder.startUsbDiscovery()
46
+ }
47
+
48
+ override fun stopUsbDiscovery() {
49
+ YubiKitManagerHolder.stopUsbDiscovery()
50
+ }
51
+
52
+ override fun startNfcDiscovery(config: ReadableMap?) {
53
+ YubiKitManagerHolder.initialize(reactApplicationContext)
54
+ val nfcConfig = NfcConfiguration()
55
+ config?.let {
56
+ if (it.hasKey("timeout") && !it.isNull("timeout")) {
57
+ nfcConfig.timeout(it.getInt("timeout"))
58
+ }
59
+ if (it.hasKey("disableNfcDiscoverySound") && !it.isNull("disableNfcDiscoverySound")) {
60
+ nfcConfig.disableNfcDiscoverySound(it.getBoolean("disableNfcDiscoverySound"))
61
+ }
62
+ if (it.hasKey("skipNdefCheck") && !it.isNull("skipNdefCheck")) {
63
+ nfcConfig.skipNdefCheck(it.getBoolean("skipNdefCheck"))
64
+ }
65
+ if (it.hasKey("handleUnavailableNfc") && !it.isNull("handleUnavailableNfc")) {
66
+ nfcConfig.handleUnavailableNfc(it.getBoolean("handleUnavailableNfc"))
67
+ }
68
+ }
69
+ YubiKitManagerHolder.setNfcConfiguration(nfcConfig)
70
+ val activity = reactApplicationContext.currentActivity
71
+ ?: throw IllegalStateException("No current Activity for NFC discovery")
72
+ YubiKitManagerHolder.startNfcDiscovery(activity)
73
+ }
74
+
75
+ override fun stopNfcDiscovery() {
76
+ val activity = reactApplicationContext.currentActivity
77
+ ?: throw IllegalStateException("No current Activity for NFC discovery")
78
+ YubiKitManagerHolder.stopNfcDiscovery(activity)
79
+ }
80
+
81
+ override fun requestConnection(deviceHandle: String, connectionType: String, promise: Promise) {
82
+ moduleScope.launch {
83
+ try {
84
+ val clazz = when (connectionType) {
85
+ "SmartCardConnection" -> SmartCardConnection::class.java
86
+ "OtpConnection" -> OtpConnection::class.java
87
+ "FidoConnection" -> FidoConnection::class.java
88
+ else -> throw IllegalArgumentException("Unknown connection type: $connectionType")
89
+ }
90
+ var result: Result<Pair<String, com.yubico.yubikit.core.YubiKeyConnection>>? = null
91
+ YubiKitManagerHolder.requestConnection(deviceHandle, clazz) { res ->
92
+ result = res
93
+ }
94
+ // Wait briefly for callback (requestConnection callback is typically synchronous-ish)
95
+ withContext(Dispatchers.IO) {
96
+ var waited = 0
97
+ while (result == null && waited < 5000) {
98
+ Thread.sleep(50)
99
+ waited += 50
100
+ }
101
+ }
102
+ val pair = result?.getOrThrow()
103
+ ?: throw IOException("Timeout waiting for connection")
104
+ promise.resolve(pair.first)
105
+ } catch (e: Exception) {
106
+ promise.reject("CONNECTION_ERROR", e.message, e)
107
+ }
108
+ }
109
+ }
110
+
111
+ override fun sendApdu(connectionHandle: String, apdu: String, promise: Promise) {
112
+ moduleScope.launch {
113
+ try {
114
+ val connection = YubiKitManagerHolder.getConnection(connectionHandle)
115
+ if (connection !is SmartCardConnection) {
116
+ throw IllegalArgumentException("sendApdu requires a SmartCardConnection")
117
+ }
118
+ val data = base64Decode(apdu) ?: byteArrayOf()
119
+ val response = connection.sendAndReceive(data)
120
+ promise.resolve(base64Encode(response))
121
+ } catch (e: Exception) {
122
+ promise.reject("APDU_ERROR", e.message, e)
123
+ }
124
+ }
125
+ }
126
+
127
+ override fun closeConnection(connectionHandle: String) {
128
+ YubiKitManagerHolder.removeConnection(connectionHandle)
129
+ }
130
+
131
+ override fun getDiscoveredDevices(): WritableArray {
132
+ val array = Arguments.createArray()
133
+ YubiKitManagerHolder.listDevices().forEach { (handle, device) ->
134
+ array.pushMap(deviceToBundle(handle, device).toWritableMap())
135
+ }
136
+ return array
137
+ }
138
+
139
+ companion object {
140
+ const val NAME = "YubikitCore"
141
+ }
142
+ }
@@ -0,0 +1,418 @@
1
+ package com.yubikit
2
+
3
+ import android.util.Base64
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.Promise
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.bridge.WritableMap
9
+ import com.yubico.yubikit.core.YubiKeyConnection
10
+ import com.yubico.yubikit.core.fido.FidoConnection
11
+ import com.yubico.yubikit.core.smartcard.SmartCardConnection
12
+ import com.yubico.yubikit.fido.client.Ctap2Client
13
+ import com.yubico.yubikit.fido.client.CredentialManager
14
+ import com.yubico.yubikit.fido.client.WebAuthnClient
15
+ import com.yubico.yubikit.fido.client.clientdata.ClientDataProvider
16
+ import com.yubico.yubikit.fido.ctap.Ctap2Session
17
+ import com.yubico.yubikit.fido.webauthn.AuthenticatorAssertionResponse
18
+ import com.yubico.yubikit.fido.webauthn.AuthenticatorAttestationResponse
19
+ import com.yubico.yubikit.fido.webauthn.AuthenticatorSelectionCriteria
20
+ import com.yubico.yubikit.fido.webauthn.Extensions
21
+ import com.yubico.yubikit.fido.webauthn.PublicKeyCredential
22
+ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions
23
+ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialDescriptor
24
+ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialParameters
25
+ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions
26
+ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRpEntity
27
+ import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialUserEntity
28
+ import com.yubikit.utils.YubikitUtils.base64Decode
29
+ import com.yubikit.utils.YubikitUtils.base64Encode
30
+ import kotlinx.coroutines.CoroutineScope
31
+ import kotlinx.coroutines.Dispatchers
32
+ import kotlinx.coroutines.launch
33
+ import org.json.JSONObject
34
+
35
+ @Suppress("unused")
36
+ class YubikitFidoModule(reactContext: ReactApplicationContext) :
37
+ NativeYubikitFidoSpec(reactContext) {
38
+
39
+ private val moduleScope = CoroutineScope(Dispatchers.IO)
40
+
41
+ override fun getName(): String = NAME
42
+
43
+ // NfcYubiKeyDevice only ever supports SmartCardConnection, so hardcoding
44
+ // FidoConnection here would break every CTAP2/FIDO2 operation over NFC.
45
+ // Ctap2Session accepts either connection type, so prefer FidoConnection (USB)
46
+ // and fall back to SmartCardConnection (NFC and USB devices without CCID).
47
+ private val ctap2ConnectionOrder = listOf(
48
+ FidoConnection::class.java as Class<out YubiKeyConnection>,
49
+ SmartCardConnection::class.java
50
+ )
51
+
52
+ private fun openCtap2Session(connection: YubiKeyConnection): Ctap2Session {
53
+ return when (connection) {
54
+ is FidoConnection -> Ctap2Session(connection)
55
+ is SmartCardConnection -> Ctap2Session(connection)
56
+ else -> throw IllegalArgumentException("Unsupported connection type for CTAP2: $connection")
57
+ }
58
+ }
59
+
60
+ private inline fun <R> withFidoConnection(
61
+ deviceHandle: String,
62
+ promise: Promise,
63
+ crossinline block: (YubiKeyConnection) -> R
64
+ ) {
65
+ moduleScope.launch {
66
+ try {
67
+ val result = YubiKitManagerHolder.withAnyConnection(deviceHandle, ctap2ConnectionOrder) { connection -> block(connection) }
68
+ when (result) {
69
+ is Unit -> promise.resolve(null)
70
+ else -> promise.resolve(result)
71
+ }
72
+ } catch (e: Exception) {
73
+ promise.reject("FIDO_ERROR", e.message, e)
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * The SDK only accepts a pre-built clientDataJSON hash, so the standard WebAuthn
80
+ * clientDataJSON must be constructed here from the JS-supplied challenge.
81
+ */
82
+ private fun base64UrlEncode(bytes: ByteArray): String {
83
+ return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
84
+ }
85
+
86
+ private fun buildClientData(type: String, challenge: ByteArray, effectiveDomain: String): ClientDataProvider {
87
+ val json = JSONObject().apply {
88
+ put("type", type)
89
+ put("challenge", base64UrlEncode(challenge))
90
+ put("origin", "https://$effectiveDomain")
91
+ put("crossOrigin", false)
92
+ }
93
+ return ClientDataProvider.fromClientDataJson(json.toString().toByteArray(Charsets.UTF_8))
94
+ }
95
+
96
+ private fun mapToPublicKeyCredential(cred: PublicKeyCredential): WritableMap {
97
+ return Arguments.createMap().apply {
98
+ putString("id", base64Encode(cred.rawId))
99
+ putString("rawId", base64Encode(cred.rawId))
100
+ putString("type", "public-key")
101
+ putMap("response", Arguments.createMap().apply {
102
+ val response = cred.response
103
+ putString("clientDataJSON", base64Encode(response.clientDataJson))
104
+ when (response) {
105
+ is AuthenticatorAttestationResponse -> {
106
+ putString("authenticatorData", base64Encode(response.authenticatorData.bytes))
107
+ putString("attestationObject", base64Encode(response.attestationObject))
108
+ }
109
+ is AuthenticatorAssertionResponse -> {
110
+ putString("authenticatorData", base64Encode(response.authenticatorData))
111
+ putString("signature", base64Encode(response.signature))
112
+ response.userHandle?.let { putString("userHandle", base64Encode(it)) }
113
+ }
114
+ }
115
+ })
116
+ }
117
+ }
118
+
119
+ private fun mapCtap2Info(info: Ctap2Session.InfoData): WritableMap {
120
+ return Arguments.createMap().apply {
121
+ val versions = Arguments.createArray()
122
+ info.versions.forEach { versions.pushString(it) }
123
+ putArray("versions", versions)
124
+ putString("aaguid", base64Encode(info.aaguid))
125
+ val extensions = Arguments.createArray()
126
+ info.extensions.forEach { extensions.pushString(it) }
127
+ putArray("extensions", extensions)
128
+ val optsMap = Arguments.createMap()
129
+ info.options.forEach { (k, v) -> if (v is Boolean) optsMap.putBoolean(k, v) }
130
+ putMap("options", optsMap)
131
+ putInt("maxMsgSize", info.maxMsgSize)
132
+ val protos = Arguments.createArray()
133
+ info.pinUvAuthProtocols.forEach { protos.pushInt(it) }
134
+ putArray("pinUvAuthProtocols", protos)
135
+ info.maxCredentialCountInList?.let { putInt("maxCredentialCountInList", it) }
136
+ info.maxCredentialIdLength?.let { putInt("maxCredentialIdLength", it) }
137
+ val transports = Arguments.createArray()
138
+ info.transports.forEach { transports.pushString(it) }
139
+ putArray("transports", transports)
140
+ val algorithms = Arguments.createArray()
141
+ info.algorithms.forEach { alg ->
142
+ algorithms.pushMap(Arguments.createMap().apply {
143
+ putString("type", alg.type)
144
+ putInt("alg", alg.alg)
145
+ })
146
+ }
147
+ putArray("algorithms", algorithms)
148
+ putInt("maxLargeBlob", info.maxSerializedLargeBlobArray)
149
+ putInt("minPinLength", info.minPinLength)
150
+ putInt("maxCredBlobLength", info.maxCredBlobLength)
151
+ putInt("maxRpidsForMinPinLength", info.maxRpidsForSetMinPinLength)
152
+ info.preferredPlatformUvAttempts?.let { putInt("preferredPlatformUvAttempts", it) }
153
+ putInt("uvModality", info.uvModality)
154
+ val certsMap = Arguments.createMap()
155
+ info.certifications.forEach { (k, v) -> if (v is Int) certsMap.putInt(k, v) }
156
+ putMap("certifications", certsMap)
157
+ info.remainingDiscoverableCredentials?.let { putInt("remainingDiscoverableCredentials", it) }
158
+ info.vendorPrototypeConfigCommands?.let { cmds ->
159
+ val arr = Arguments.createArray()
160
+ cmds.forEach { arr.pushInt(it) }
161
+ putArray("vendorPrototypeConfigCommands", arr)
162
+ }
163
+ }
164
+ }
165
+
166
+ override fun getInfo(deviceHandle: String, promise: Promise) {
167
+ withFidoConnection(deviceHandle, promise) { connection ->
168
+ openCtap2Session(connection).use { session ->
169
+ mapCtap2Info(session.info)
170
+ }
171
+ }
172
+ }
173
+
174
+ override fun makeCredential(
175
+ deviceHandle: String,
176
+ options: ReadableMap,
177
+ effectiveDomain: String,
178
+ pin: String?,
179
+ enterpriseAttestation: Double?,
180
+ promise: Promise
181
+ ) {
182
+ moduleScope.launch {
183
+ try {
184
+ val credential = YubiKitManagerHolder.withAnyConnection(deviceHandle, ctap2ConnectionOrder) { connection ->
185
+ WebAuthnClient.create(connection, null, null).use { client ->
186
+ val rpMap = options.getMap("rp")!!
187
+ val userMap = options.getMap("user")!!
188
+ val challenge = base64Decode(options.getString("challenge")) ?: byteArrayOf()
189
+ val clientData = buildClientData("webauthn.create", challenge, effectiveDomain)
190
+
191
+ val pubKeyCredParams = options.getArray("pubKeyCredParams")!!.let { arr ->
192
+ (0 until arr.size()).map { i ->
193
+ val m = arr.getMap(i)!!
194
+ PublicKeyCredentialParameters(
195
+ m.getString("type") ?: "public-key",
196
+ m.getInt("alg")
197
+ )
198
+ }
199
+ }
200
+
201
+ val rp = PublicKeyCredentialRpEntity(
202
+ rpMap.getString("name") ?: rpMap.getString("id") ?: effectiveDomain,
203
+ rpMap.getString("id") ?: effectiveDomain
204
+ )
205
+ val user = PublicKeyCredentialUserEntity(
206
+ userMap.getString("name") ?: "",
207
+ base64Decode(userMap.getString("id")) ?: byteArrayOf(),
208
+ userMap.getString("displayName") ?: ""
209
+ )
210
+
211
+ val excludeCredentials = options.getArray("excludeCredentials")?.let { arr ->
212
+ (0 until arr.size()).map { i ->
213
+ val m = arr.getMap(i)!!
214
+ PublicKeyCredentialDescriptor(
215
+ m.getString("type") ?: "public-key",
216
+ base64Decode(m.getString("id")) ?: byteArrayOf(),
217
+ m.getArray("transports")?.toArrayList()?.map { it.toString() }
218
+ )
219
+ }
220
+ }
221
+
222
+ val authenticatorSelection = options.getMap("authenticatorSelection")?.let { sel ->
223
+ AuthenticatorSelectionCriteria(
224
+ sel.getString("authenticatorAttachment"),
225
+ sel.getString("residentKey"),
226
+ sel.getString("userVerification")
227
+ )
228
+ }
229
+
230
+ val extensions = options.getMap("extensions")?.toHashMap()?.let { Extensions.fromMap(it) }
231
+
232
+ val creationOptions = PublicKeyCredentialCreationOptions(
233
+ rp,
234
+ user,
235
+ challenge,
236
+ pubKeyCredParams,
237
+ if (options.hasKey("timeout") && !options.isNull("timeout")) options.getDouble("timeout").toLong() else null,
238
+ excludeCredentials,
239
+ authenticatorSelection,
240
+ options.getString("attestation"),
241
+ extensions
242
+ )
243
+
244
+ client.makeCredential(
245
+ clientData,
246
+ creationOptions,
247
+ effectiveDomain,
248
+ pin?.toCharArray(),
249
+ enterpriseAttestation?.toInt(),
250
+ null
251
+ )
252
+ }
253
+ }
254
+ promise.resolve(mapToPublicKeyCredential(credential))
255
+ } catch (e: Exception) {
256
+ promise.reject("FIDO_ERROR", e.message, e)
257
+ }
258
+ }
259
+ }
260
+
261
+ override fun getAssertion(
262
+ deviceHandle: String,
263
+ options: ReadableMap,
264
+ effectiveDomain: String,
265
+ pin: String?,
266
+ promise: Promise
267
+ ) {
268
+ moduleScope.launch {
269
+ try {
270
+ val credential = YubiKitManagerHolder.withAnyConnection(deviceHandle, ctap2ConnectionOrder) { connection ->
271
+ WebAuthnClient.create(connection, null, null).use { client ->
272
+ val challenge = base64Decode(options.getString("challenge")) ?: byteArrayOf()
273
+ val clientData = buildClientData("webauthn.get", challenge, effectiveDomain)
274
+ val rpId = options.getString("rpId") ?: effectiveDomain
275
+
276
+ val allowCredentials = options.getArray("allowCredentials")?.let { arr ->
277
+ (0 until arr.size()).map { i ->
278
+ val m = arr.getMap(i)!!
279
+ PublicKeyCredentialDescriptor(
280
+ m.getString("type") ?: "public-key",
281
+ base64Decode(m.getString("id")) ?: byteArrayOf(),
282
+ m.getArray("transports")?.toArrayList()?.map { it.toString() }
283
+ )
284
+ }
285
+ }
286
+
287
+ val extensions = options.getMap("extensions")?.toHashMap()?.let { Extensions.fromMap(it) }
288
+
289
+ val requestOptions = PublicKeyCredentialRequestOptions(
290
+ challenge,
291
+ if (options.hasKey("timeout") && !options.isNull("timeout")) options.getDouble("timeout").toLong() else null,
292
+ rpId,
293
+ allowCredentials,
294
+ options.getString("userVerification"),
295
+ extensions
296
+ )
297
+
298
+ client.getAssertion(clientData, requestOptions, effectiveDomain, pin?.toCharArray(), null)
299
+ }
300
+ }
301
+ promise.resolve(mapToPublicKeyCredential(credential))
302
+ } catch (e: Exception) {
303
+ promise.reject("FIDO_ERROR", e.message, e)
304
+ }
305
+ }
306
+ }
307
+
308
+ override fun reset(deviceHandle: String, promise: Promise) {
309
+ withFidoConnection(deviceHandle, promise) { connection ->
310
+ openCtap2Session(connection).use { it.reset(null) }
311
+ }
312
+ }
313
+
314
+ private fun withCredentialManager(
315
+ deviceHandle: String,
316
+ pin: String,
317
+ promise: Promise,
318
+ block: (CredentialManager) -> Any?
319
+ ) {
320
+ moduleScope.launch {
321
+ try {
322
+ val result = YubiKitManagerHolder.withAnyConnection(deviceHandle, ctap2ConnectionOrder) { connection ->
323
+ Ctap2Client(openCtap2Session(connection)).use { client ->
324
+ val manager = client.getCredentialManager(pin.toCharArray())
325
+ block(manager)
326
+ }
327
+ }
328
+ when (result) {
329
+ is Unit -> promise.resolve(null)
330
+ else -> promise.resolve(result)
331
+ }
332
+ } catch (e: Exception) {
333
+ promise.reject("FIDO_ERROR", e.message, e)
334
+ }
335
+ }
336
+ }
337
+
338
+ override fun getCredentialCount(deviceHandle: String, pin: String, promise: Promise) {
339
+ withCredentialManager(deviceHandle, pin, promise) { it.credentialCount }
340
+ }
341
+
342
+ override fun getRpIdList(deviceHandle: String, pin: String, promise: Promise) {
343
+ withCredentialManager(deviceHandle, pin, promise) {
344
+ val list = it.rpIdList
345
+ val arr = Arguments.createArray()
346
+ list.forEach { rpId -> arr.pushString(rpId) }
347
+ arr
348
+ }
349
+ }
350
+
351
+ override fun getCredentials(
352
+ deviceHandle: String,
353
+ rpId: String,
354
+ pin: String,
355
+ promise: Promise
356
+ ) {
357
+ withCredentialManager(deviceHandle, pin, promise) { manager ->
358
+ val creds = manager.getCredentials(rpId)
359
+ val arr = Arguments.createArray()
360
+ creds.forEach { (descriptor, user) ->
361
+ arr.pushMap(Arguments.createMap().apply {
362
+ putMap("credential", Arguments.createMap().apply {
363
+ putString("id", base64Encode(descriptor.id))
364
+ putString("type", descriptor.type)
365
+ })
366
+ putMap("user", Arguments.createMap().apply {
367
+ putString("id", base64Encode(user.id))
368
+ putString("name", user.name)
369
+ putString("displayName", user.displayName)
370
+ })
371
+ })
372
+ }
373
+ arr
374
+ }
375
+ }
376
+
377
+ override fun deleteCredential(
378
+ deviceHandle: String,
379
+ credential: ReadableMap,
380
+ pin: String,
381
+ promise: Promise
382
+ ) {
383
+ withCredentialManager(deviceHandle, pin, promise) { manager ->
384
+ manager.deleteCredential(
385
+ PublicKeyCredentialDescriptor(
386
+ credential.getString("type") ?: "public-key",
387
+ base64Decode(credential.getString("id")) ?: byteArrayOf()
388
+ )
389
+ )
390
+ }
391
+ }
392
+
393
+ override fun updateUserInformation(
394
+ deviceHandle: String,
395
+ credential: ReadableMap,
396
+ user: ReadableMap,
397
+ pin: String,
398
+ promise: Promise
399
+ ) {
400
+ withCredentialManager(deviceHandle, pin, promise) { manager ->
401
+ manager.updateUserInformation(
402
+ PublicKeyCredentialDescriptor(
403
+ credential.getString("type") ?: "public-key",
404
+ base64Decode(credential.getString("id")) ?: byteArrayOf()
405
+ ),
406
+ PublicKeyCredentialUserEntity(
407
+ user.getString("name") ?: "",
408
+ base64Decode(user.getString("id")) ?: byteArrayOf(),
409
+ user.getString("displayName") ?: ""
410
+ )
411
+ )
412
+ }
413
+ }
414
+
415
+ companion object {
416
+ const val NAME = "YubikitFido"
417
+ }
418
+ }
@@ -0,0 +1,132 @@
1
+ package com.yubikit
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.yubico.yubikit.core.YubiKeyConnection
7
+ import com.yubico.yubikit.core.fido.FidoConnection
8
+ import com.yubico.yubikit.core.otp.OtpConnection
9
+ import com.yubico.yubikit.core.smartcard.SmartCardConnection
10
+ import com.yubico.yubikit.management.ManagementSession
11
+ import com.yubico.yubikit.core.UsbInterface
12
+ import com.yubikit.utils.YubikitUtils.base64Decode
13
+ import com.yubikit.utils.YubikitUtils.deviceInfoToMap
14
+ import com.yubikit.utils.YubikitUtils.parseDeviceConfig
15
+ import com.yubikit.utils.YubikitUtils.parseUsbMode
16
+ import kotlinx.coroutines.CoroutineScope
17
+ import kotlinx.coroutines.Dispatchers
18
+ import kotlinx.coroutines.launch
19
+
20
+ @Suppress("unused")
21
+ class YubikitManagementModule(reactContext: ReactApplicationContext) :
22
+ NativeYubikitManagementSpec(reactContext) {
23
+
24
+ private val moduleScope = CoroutineScope(Dispatchers.IO)
25
+
26
+ // ManagementSession supports SmartCard, Otp and Fido connections; hardcoding
27
+ // SmartCardConnection breaks OTP-only and FIDO-only-mode devices (e.g. a YubiKey
28
+ // with CCID disabled via USB mode config).
29
+ private val managementConnectionOrder = listOf(
30
+ SmartCardConnection::class.java as Class<out YubiKeyConnection>,
31
+ OtpConnection::class.java,
32
+ FidoConnection::class.java
33
+ )
34
+
35
+ private fun openManagementSession(connection: YubiKeyConnection): ManagementSession {
36
+ return when (connection) {
37
+ is SmartCardConnection -> ManagementSession(connection)
38
+ is OtpConnection -> ManagementSession(connection)
39
+ is FidoConnection -> ManagementSession(connection)
40
+ else -> throw IllegalArgumentException("Unsupported connection type for Management: $connection")
41
+ }
42
+ }
43
+
44
+ override fun getName(): String = NAME
45
+
46
+ override fun getDeviceInfo(deviceHandle: String, promise: Promise) {
47
+ moduleScope.launch {
48
+ try {
49
+ val info = YubiKitManagerHolder.withAnyConnection(deviceHandle, managementConnectionOrder) { connection ->
50
+ openManagementSession(connection).use { session ->
51
+ session.deviceInfo
52
+ }
53
+ }
54
+ promise.resolve(deviceInfoToMap(info))
55
+ } catch (e: Exception) {
56
+ promise.reject("MANAGEMENT_ERROR", e.message, e)
57
+ }
58
+ }
59
+ }
60
+
61
+ override fun updateDeviceConfig(
62
+ deviceHandle: String,
63
+ config: ReadableMap,
64
+ reboot: Boolean,
65
+ currentLockCode: String?,
66
+ newLockCode: String?,
67
+ promise: Promise
68
+ ) {
69
+ moduleScope.launch {
70
+ try {
71
+ val deviceConfig = parseDeviceConfig(config)
72
+ YubiKitManagerHolder.withAnyConnection(deviceHandle, managementConnectionOrder) { connection ->
73
+ openManagementSession(connection).use { session ->
74
+ session.updateDeviceConfig(
75
+ deviceConfig,
76
+ reboot,
77
+ base64Decode(currentLockCode),
78
+ base64Decode(newLockCode)
79
+ )
80
+ }
81
+ }
82
+ promise.resolve(null)
83
+ } catch (e: Exception) {
84
+ promise.reject("MANAGEMENT_ERROR", e.message, e)
85
+ }
86
+ }
87
+ }
88
+
89
+ override fun setMode(
90
+ deviceHandle: String,
91
+ mode: String,
92
+ chalrespTimeout: Double,
93
+ autoejectTimeout: Double,
94
+ promise: Promise
95
+ ) {
96
+ moduleScope.launch {
97
+ try {
98
+ YubiKitManagerHolder.withAnyConnection(deviceHandle, managementConnectionOrder) { connection ->
99
+ openManagementSession(connection).use { session ->
100
+ session.setMode(
101
+ parseUsbMode(mode),
102
+ chalrespTimeout.toInt().toByte(),
103
+ autoejectTimeout.toInt().toShort()
104
+ )
105
+ }
106
+ }
107
+ promise.resolve(null)
108
+ } catch (e: Exception) {
109
+ promise.reject("MANAGEMENT_ERROR", e.message, e)
110
+ }
111
+ }
112
+ }
113
+
114
+ override fun deviceReset(deviceHandle: String, promise: Promise) {
115
+ moduleScope.launch {
116
+ try {
117
+ YubiKitManagerHolder.withAnyConnection(deviceHandle, managementConnectionOrder) { connection ->
118
+ openManagementSession(connection).use { session ->
119
+ session.deviceReset()
120
+ }
121
+ }
122
+ promise.resolve(null)
123
+ } catch (e: Exception) {
124
+ promise.reject("MANAGEMENT_ERROR", e.message, e)
125
+ }
126
+ }
127
+ }
128
+
129
+ companion object {
130
+ const val NAME = "YubikitManagement"
131
+ }
132
+ }