@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
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quan Pham
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ react-native-yubikit
2
+
3
+ A React Native TurboModule wrapper around Yubico's native [YubiKit Android](https://developers.yubico.com/yubikit-android/) and [YubiKit iOS](https://developers.yubico.com/yubikit-ios/) SDKs, for talking to YubiKey hardware over USB and NFC.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@doko/react-native-yubikit?style=for-the-badge&color=blue)](https://www.npmjs.com/package/@doko/react-native-yubikit)
6
+ [![Monthly downloads](https://img.shields.io/npm/dm/@doko/react-native-yubikit?style=for-the-badge)](https://www.npmjs.com/package/@doko/react-native-yubikit)
7
+ [![New Architecture](https://img.shields.io/badge/New%20Architecture-Only-5f3dc4?style=for-the-badge)](https://reactnative.dev/docs/the-new-architecture/landing-page)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Supported-3178C6?style=for-the-badge)](https://www.typescriptlang.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-2f9e44?style=for-the-badge)](LICENSE)
10
+ [![iOS](https://img.shields.io/badge/iOS-16%2B-000000?style=for-the-badge&logo=apple)](https://developer.apple.com/ios/)
11
+ [![Android](https://img.shields.io/badge/Android-API%2024%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://developer.android.com/)
12
+
13
+ 📖 **Full documentation:** [doko.aniviet.com/oss/react-native-yubikey](https://doko.aniviet.com/oss/react-native-yubikey)
14
+
15
+ The docs site covers requirements, installation (including the iOS Podfile override you need), usage examples for every module, security notes, advanced patterns, and troubleshooting. This README is deliberately short - start there for anything beyond a quick look.
16
+
17
+ ---
18
+
19
+ ## Features
20
+
21
+ - 🔌 **USB + NFC discovery** - attach/detach events via a single `YubiKeyEvent` stream
22
+ - 🧩 **8 native modules**, exposed as namespaces: `Core`, `Support`, `Management`, `Oath`, `Piv`, `OpenPgp`, `YubiOtp`, `Fido`
23
+ - 🪝 **`YubiKeyProvider` + `useYubiKey()`** - drop-in device discovery/selection state, no boilerplate required
24
+ - 🔐 **PIV** - PIN/PUK, slot metadata, certificates, key generation, raw sign/decrypt
25
+ - 🔑 **FIDO2/WebAuthn** - authenticator info, registration, authentication
26
+ - ⏱️ **OATH** - TOTP/HOTP credential management and code calculation
27
+ - 🆕 **New Architecture only** - built as Fabric/TurboModules with Codegen, no old-bridge fallback
28
+
29
+ ---
30
+
31
+ ## Platform support
32
+
33
+ Android has full parity across every module. iOS is missing OpenPGP entirely, YubiOTP slot programming, and FIDO2 resident-credential management - these gaps come from the underlying YubiKit iOS SDK itself, not from this wrapper. See the [full platform support table](https://doko.aniviet.com/oss/react-native-yubikey) on the docs site for the exact per-method breakdown.
34
+
35
+ | Module | Android | iOS |
36
+ | -------------- | ------- | --------------------------------------- |
37
+ | Core / Support | Full | Full |
38
+ | Management | Full | Partial |
39
+ | OATH | Full | Full |
40
+ | PIV | Full | Full (no RSA3072/4096 raw sign/decrypt) |
41
+ | OpenPGP | Full | Not available |
42
+ | YubiOTP | Full | Partial (HMAC-SHA1 only) |
43
+ | FIDO2 | Full | Partial (no credential management) |
44
+
45
+ ---
46
+
47
+ ## Requirements
48
+
49
+ | | Minimum |
50
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | React Native | 0.74+ with the **New Architecture enabled** |
52
+ | iOS | 16.4+ recommended ([Podfile override required](https://doko.aniviet.com/oss/react-native-yubikey/installation) - the CocoaPods-trunk `YubiKit` pod is too old) |
53
+ | Android | `minSdkVersion` 24 |
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ```sh
60
+ npm install @doko/react-native-yubikit
61
+ # or
62
+ yarn add @doko/react-native-yubikit
63
+ # or
64
+ pnpm add @doko/react-native-yubikit
65
+ ```
66
+
67
+ iOS needs a Podfile override and a few entitlements depending on which transports you use; Android needs USB/NFC manifest entries. See [Installation](https://doko.aniviet.com/oss/react-native-yubikey/installation) for the exact steps.
68
+
69
+ ---
70
+
71
+ ## Quick start
72
+
73
+ ```tsx
74
+ import { YubiKeyProvider, useYubiKey } from '@doko/react-native-yubikit';
75
+
76
+ export default function App() {
77
+ return (
78
+ <YubiKeyProvider>
79
+ <DeviceScreen />
80
+ </YubiKeyProvider>
81
+ );
82
+ }
83
+
84
+ function DeviceScreen() {
85
+ const { devices, selectedDevice, startUsbDiscovery, stopUsbDiscovery } =
86
+ useYubiKey();
87
+
88
+ // startUsbDiscovery({ handlePermissions: true }) to begin listening,
89
+ // then read/write against selectedDevice.handle with any module below.
90
+ }
91
+ ```
92
+
93
+ ```ts
94
+ import { Management, Oath, Piv } from '@doko/react-native-yubikit';
95
+
96
+ const info = await Management.getDeviceInfo(deviceHandle);
97
+ await Piv.verifyPin(deviceHandle, '123456');
98
+ const { credentials } = await Oath.getCredentials(deviceHandle);
99
+ ```
100
+
101
+ More examples for every module are in [Usage Examples](https://doko.aniviet.com/oss/react-native-yubikey/usage).
102
+
103
+ ---
104
+
105
+ ## Example app
106
+
107
+ A runnable example app is included in [`example/`](./example) (Expo Router, demonstrates every supported module).
108
+
109
+ ```sh
110
+ pnpm install
111
+ pnpm example start
112
+ ```
113
+
114
+ Then press `a` for Android or `i` for iOS.
115
+
116
+ ---
117
+
118
+ ## Contributing
119
+
120
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow, build instructions, and how to send a pull request.
121
+
122
+ ---
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,22 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "RNYubikit"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/Doko-Demo-Doa/react-native-yubikit.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ s.dependency "YubiKit"
20
+
21
+ install_modules_dependencies(s)
22
+ end
@@ -0,0 +1,63 @@
1
+ buildscript {
2
+ ext.Yubikit = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36
6
+ ]
7
+
8
+ ext.getExtOrDefault = { prop ->
9
+ if (rootProject.ext.has(prop)) {
10
+ return rootProject.ext.get(prop)
11
+ }
12
+
13
+ return Yubikit[prop]
14
+ }
15
+
16
+ repositories {
17
+ google()
18
+ mavenCentral()
19
+ }
20
+
21
+ dependencies {
22
+ classpath "com.android.tools.build:gradle:8.7.2"
23
+ // noinspection DifferentKotlinGradleVersion
24
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
25
+ }
26
+ }
27
+
28
+
29
+ apply plugin: "com.android.library"
30
+ apply plugin: "kotlin-android"
31
+
32
+ apply plugin: "com.facebook.react"
33
+
34
+ android {
35
+ namespace "com.yubikit"
36
+
37
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
38
+
39
+ defaultConfig {
40
+ minSdkVersion getExtOrDefault("minSdkVersion")
41
+ }
42
+
43
+ compileOptions {
44
+ sourceCompatibility JavaVersion.VERSION_17
45
+ targetCompatibility JavaVersion.VERSION_17
46
+ }
47
+ }
48
+
49
+ dependencies {
50
+ implementation "com.facebook.react:react-android"
51
+
52
+ def yubikitVersion = "3.1.0"
53
+ implementation "com.yubico.yubikit:android:${yubikitVersion}"
54
+ implementation "com.yubico.yubikit:core:${yubikitVersion}"
55
+ implementation "com.yubico.yubikit:fido:${yubikitVersion}"
56
+ implementation "com.yubico.yubikit:fido-android-ui:${yubikitVersion}"
57
+ implementation "com.yubico.yubikit:management:${yubikitVersion}"
58
+ implementation "com.yubico.yubikit:yubiotp:${yubikitVersion}"
59
+ implementation "com.yubico.yubikit:openpgp:${yubikitVersion}"
60
+ implementation "com.yubico.yubikit:oath:${yubikitVersion}"
61
+ implementation "com.yubico.yubikit:piv:${yubikitVersion}"
62
+ implementation "com.yubico.yubikit:support:${yubikitVersion}"
63
+ }
@@ -0,0 +1,9 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- YubiKey USB support -->
3
+ <uses-feature android:name="android.hardware.usb.host" android:required="false" />
4
+ <uses-permission android:name="android.permission.NFC" />
5
+
6
+ <application>
7
+ <!-- YubiKit uses USB device detection via the host APIs; no manifest receivers are required. -->
8
+ </application>
9
+ </manifest>
@@ -0,0 +1,330 @@
1
+ package com.yubikit
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.bridge.WritableMap
7
+ import com.yubico.yubikit.android.YubiKitManager
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.nfc.NfcYubiKeyDevice
11
+ import com.yubico.yubikit.android.transport.usb.UsbConfiguration
12
+ import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
13
+ import com.yubico.yubikit.core.YubiKeyConnection
14
+ import com.yubico.yubikit.core.YubiKeyDevice
15
+ import com.yubico.yubikit.core.fido.FidoConnection
16
+ import com.yubico.yubikit.core.otp.OtpConnection
17
+ import com.yubico.yubikit.core.smartcard.SmartCardConnection
18
+ import com.yubikit.utils.YubikitUtils.toWritableMap
19
+ import java.io.IOException
20
+ import java.util.UUID
21
+ import java.util.concurrent.ConcurrentHashMap
22
+ import java.util.concurrent.CountDownLatch
23
+ import java.util.concurrent.TimeUnit
24
+ import java.util.concurrent.atomic.AtomicReference
25
+
26
+ /**
27
+ * Singleton holder for the YubiKitManager and discovered devices/connections.
28
+ */
29
+ object YubiKitManagerHolder {
30
+ @Volatile
31
+ private var manager: YubiKitManager? = null
32
+
33
+ private val devices = ConcurrentHashMap<String, YubiKeyDevice>()
34
+ private val connections = ConcurrentHashMap<String, YubiKeyConnection>()
35
+ private val connectionReleaseLatches = ConcurrentHashMap<String, CountDownLatch>()
36
+
37
+ private var usbConfiguration = UsbConfiguration()
38
+ private var nfcConfiguration = NfcConfiguration()
39
+
40
+ @JvmStatic
41
+ fun initialize(context: Context) {
42
+ if (manager == null) {
43
+ synchronized(this) {
44
+ if (manager == null) {
45
+ manager = YubiKitManager(context.applicationContext)
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ @JvmStatic
52
+ fun getManager(): YubiKitManager {
53
+ return manager ?: throw IllegalStateException("YubiKitManager not initialized")
54
+ }
55
+
56
+ @JvmStatic
57
+ fun putDevice(device: YubiKeyDevice): String {
58
+ val handle = UUID.randomUUID().toString()
59
+ devices[handle] = device
60
+ return handle
61
+ }
62
+
63
+ @JvmStatic
64
+ fun getDevice(handle: String): YubiKeyDevice {
65
+ return devices[handle] ?: throw IllegalArgumentException("Unknown device handle: $handle")
66
+ }
67
+
68
+ @JvmStatic
69
+ fun removeDevice(handle: String) {
70
+ devices.remove(handle)
71
+ }
72
+
73
+ @JvmStatic
74
+ fun listDevices(): List<Pair<String, YubiKeyDevice>> {
75
+ return devices.toList()
76
+ }
77
+
78
+ @JvmStatic
79
+ fun putConnection(connection: YubiKeyConnection): String {
80
+ val handle = UUID.randomUUID().toString()
81
+ connections[handle] = connection
82
+ return handle
83
+ }
84
+
85
+ @JvmStatic
86
+ fun getConnection(handle: String): YubiKeyConnection {
87
+ return connections[handle]
88
+ ?: throw IllegalArgumentException("Unknown connection handle: $handle")
89
+ }
90
+
91
+ @JvmStatic
92
+ fun removeConnection(handle: String) {
93
+ connections.remove(handle)?.close()
94
+ // Wakes up the requestConnection() call still blocked on this handle's
95
+ // latch below, letting its try-with-resources finally close.
96
+ connectionReleaseLatches.remove(handle)?.countDown()
97
+ }
98
+
99
+ @JvmStatic
100
+ fun closeAllConnections() {
101
+ connections.values.forEach { try { it.close() } catch (_: IOException) {} }
102
+ connections.clear()
103
+ }
104
+
105
+ @JvmStatic
106
+ fun setUsbConfiguration(config: UsbConfiguration) {
107
+ usbConfiguration = config
108
+ }
109
+
110
+ @JvmStatic
111
+ fun getUsbConfiguration(): UsbConfiguration = usbConfiguration
112
+
113
+ @JvmStatic
114
+ fun setNfcConfiguration(config: NfcConfiguration) {
115
+ nfcConfiguration = config
116
+ }
117
+
118
+ @JvmStatic
119
+ fun getNfcConfiguration(): NfcConfiguration = nfcConfiguration
120
+
121
+ // Set by YubikitCoreModule while the TurboModule instance is alive, so device
122
+ // attach/detach/failure can be routed to its generated emitOnYubiKeyEvent(...).
123
+ @Volatile
124
+ private var eventHandler: ((WritableMap) -> Unit)? = null
125
+
126
+ @JvmStatic
127
+ fun setEventHandler(handler: ((WritableMap) -> Unit)?) {
128
+ eventHandler = handler
129
+ }
130
+
131
+ @JvmStatic
132
+ fun emitDeviceAttached(handle: String, device: YubiKeyDevice) {
133
+ val params = Arguments.createMap().apply {
134
+ putString("type", "attached")
135
+ putMap("device", deviceToBundle(handle, device).toWritableMap())
136
+ }
137
+ eventHandler?.invoke(params)
138
+ }
139
+
140
+ @JvmStatic
141
+ fun emitDeviceDetached(handle: String) {
142
+ removeDevice(handle)
143
+ val params = Arguments.createMap().apply {
144
+ putString("type", "detached")
145
+ putString("handle", handle)
146
+ }
147
+ eventHandler?.invoke(params)
148
+ }
149
+
150
+ @JvmStatic
151
+ fun emitError(error: String) {
152
+ val params = Arguments.createMap().apply {
153
+ putString("type", "error")
154
+ putString("error", error)
155
+ }
156
+ eventHandler?.invoke(params)
157
+ }
158
+
159
+ @JvmStatic
160
+ fun deviceToBundle(handle: String, device: YubiKeyDevice): android.os.Bundle {
161
+ return android.os.Bundle().apply {
162
+ putString("handle", handle)
163
+ putString("transport", device.transport.name.lowercase())
164
+ putStringArrayList(
165
+ "supportedConnections",
166
+ ArrayList(
167
+ listOfNotNull(
168
+ if (device.supportsConnection(SmartCardConnection::class.java)) "SmartCardConnection" else null,
169
+ if (device.supportsConnection(OtpConnection::class.java)) "OtpConnection" else null,
170
+ if (device.supportsConnection(FidoConnection::class.java)) "FidoConnection" else null
171
+ )
172
+ )
173
+ )
174
+ }
175
+ }
176
+
177
+ @JvmStatic
178
+ fun startUsbDiscovery() {
179
+ getManager().startUsbDiscovery(usbConfiguration) { device: UsbYubiKeyDevice ->
180
+ val handle = putDevice(device)
181
+ device.setOnClosed {
182
+ emitDeviceDetached(handle)
183
+ }
184
+ emitDeviceAttached(handle, device)
185
+ }
186
+ }
187
+
188
+ @JvmStatic
189
+ fun stopUsbDiscovery() {
190
+ getManager().stopUsbDiscovery()
191
+ }
192
+
193
+ @JvmStatic
194
+ @Throws(NfcNotAvailable::class)
195
+ fun startNfcDiscovery(activity: Activity) {
196
+ getManager().startNfcDiscovery(nfcConfiguration, activity) { device: NfcYubiKeyDevice ->
197
+ val handle = putDevice(device)
198
+ emitDeviceAttached(handle, device)
199
+ }
200
+ }
201
+
202
+ @JvmStatic
203
+ fun stopNfcDiscovery(activity: Activity) {
204
+ getManager().stopNfcDiscovery(activity)
205
+ }
206
+
207
+ /**
208
+ * Open a connection synchronously. Must not be called on the UI thread.
209
+ */
210
+ @JvmStatic
211
+ fun <T : YubiKeyConnection> openConnection(
212
+ deviceHandle: String,
213
+ connectionType: Class<T>
214
+ ): Pair<String, T> {
215
+ val device = getDevice(deviceHandle)
216
+ val connection = device.openConnection(connectionType)
217
+ val handle = putConnection(connection)
218
+ return handle to connection
219
+ }
220
+
221
+ // Non-OTP connections are opened by the SDK inside a try-with-resources block that
222
+ // closes the connection the instant device.requestConnection()'s callback returns
223
+ // (see UsbYubiKeyDevice.requestConnection). Since this module's requestConnection/
224
+ // sendApdu/closeConnection are exposed as three separate, independently-timed JS
225
+ // calls, the callback below is kept alive on a latch until closeConnection() (or
226
+ // this timeout) releases it, so the connection stays open across those calls.
227
+ private const val CONNECTION_HOLD_TIMEOUT_MS = 120_000L
228
+
229
+ /**
230
+ * Open a connection asynchronously and return the handle.
231
+ */
232
+ @JvmStatic
233
+ fun requestConnection(
234
+ deviceHandle: String,
235
+ connectionType: Class<out YubiKeyConnection>,
236
+ callback: (Result<Pair<String, YubiKeyConnection>>) -> Unit
237
+ ) {
238
+ val device = getDevice(deviceHandle)
239
+ device.requestConnection(connectionType) { result ->
240
+ var handle: String? = null
241
+ try {
242
+ val connection = result.value
243
+ handle = putConnection(connection)
244
+ val releaseLatch = CountDownLatch(1)
245
+ connectionReleaseLatches[handle] = releaseLatch
246
+ callback(Result.success(handle to connection))
247
+ releaseLatch.await(CONNECTION_HOLD_TIMEOUT_MS, TimeUnit.MILLISECONDS)
248
+ } catch (e: Exception) {
249
+ callback(Result.failure(e))
250
+ } finally {
251
+ // No-op if closeConnection() already removed it; guards against the
252
+ // timeout case where the JS side never explicitly released it.
253
+ handle?.let { connections.remove(it)?.close(); connectionReleaseLatches.remove(it) }
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Run a synchronous operation on a connection inside a requestConnection callback.
260
+ * This is the preferred pattern because it automatically closes the connection.
261
+ */
262
+ @JvmStatic
263
+ fun <T : YubiKeyConnection, R> withConnection(
264
+ deviceHandle: String,
265
+ connectionType: Class<T>,
266
+ timeoutMs: Long = 30000,
267
+ block: (T) -> R
268
+ ): R {
269
+ val device = getDevice(deviceHandle)
270
+ val latch = CountDownLatch(1)
271
+ val resultRef = AtomicReference<Result<R>>()
272
+
273
+ device.requestConnection(connectionType) { result ->
274
+ try {
275
+ val connection = result.value
276
+ connection.use {
277
+ resultRef.set(Result.success(block(connection)))
278
+ }
279
+ } catch (e: Exception) {
280
+ resultRef.set(Result.failure(e))
281
+ } finally {
282
+ latch.countDown()
283
+ }
284
+ }
285
+
286
+ if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
287
+ throw IOException("Timeout waiting for YubiKey connection")
288
+ }
289
+
290
+ return resultRef.get().getOrThrow()
291
+ }
292
+
293
+ @JvmStatic
294
+ fun <R> withSmartCard(deviceHandle: String, block: (SmartCardConnection) -> R): R {
295
+ return withConnection(deviceHandle, SmartCardConnection::class.java, block = block)
296
+ }
297
+
298
+ @JvmStatic
299
+ fun <R> withOtp(deviceHandle: String, block: (OtpConnection) -> R): R {
300
+ return withConnection(deviceHandle, OtpConnection::class.java, block = block)
301
+ }
302
+
303
+ @JvmStatic
304
+ fun <R> withFido(deviceHandle: String, block: (FidoConnection) -> R): R {
305
+ return withConnection(deviceHandle, FidoConnection::class.java, block = block)
306
+ }
307
+
308
+ /**
309
+ * Opens whichever of SmartCard/Otp/Fido the device actually supports, in that
310
+ * preference order. Several sessions (ManagementSession, Ctap2Session) accept more
311
+ * than one connection type, and hardcoding a single type breaks devices/transports
312
+ * that don't support it (e.g. NFC devices only ever support SmartCardConnection).
313
+ */
314
+ @JvmStatic
315
+ fun <R> withAnyConnection(
316
+ deviceHandle: String,
317
+ preferredOrder: List<Class<out YubiKeyConnection>>,
318
+ block: (YubiKeyConnection) -> R
319
+ ): R {
320
+ val device = getDevice(deviceHandle)
321
+ val connectionType = preferredOrder.firstOrNull { device.supportsConnection(it) }
322
+ ?: throw IllegalArgumentException("Device does not support any of: ${preferredOrder.map { it.simpleName }}")
323
+ return when (connectionType) {
324
+ SmartCardConnection::class.java -> withConnection(deviceHandle, SmartCardConnection::class.java) { block(it) }
325
+ OtpConnection::class.java -> withConnection(deviceHandle, OtpConnection::class.java) { block(it) }
326
+ FidoConnection::class.java -> withConnection(deviceHandle, FidoConnection::class.java) { block(it) }
327
+ else -> throw IllegalArgumentException("Unsupported connection type: $connectionType")
328
+ }
329
+ }
330
+ }