react-native-device-defense 1.0.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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/android/build.gradle +90 -0
  4. package/android/proguard-rules.pro +28 -0
  5. package/android/src/main/AndroidManifest.xml +4 -0
  6. package/android/src/main/cpp/CMakeLists.txt +45 -0
  7. package/android/src/main/cpp/device-security.cpp +314 -0
  8. package/android/src/main/java/vn/osp/security/DebugDetection.kt +131 -0
  9. package/android/src/main/java/vn/osp/security/DeviceSecurityModule.kt +277 -0
  10. package/android/src/main/java/vn/osp/security/DeviceSecurityPackage.kt +58 -0
  11. package/android/src/main/java/vn/osp/security/EmulatorDetection.kt +204 -0
  12. package/android/src/main/java/vn/osp/security/HookDetection.kt +270 -0
  13. package/android/src/main/java/vn/osp/security/NativeSecurityCheck.kt +66 -0
  14. package/android/src/main/java/vn/osp/security/RootDetection.kt +349 -0
  15. package/lib/commonjs/NativeDeviceSecurity.js +9 -0
  16. package/lib/commonjs/NativeDeviceSecurity.js.map +1 -0
  17. package/lib/commonjs/api.js +213 -0
  18. package/lib/commonjs/api.js.map +1 -0
  19. package/lib/commonjs/components/SecurityBlockedScreen.js +177 -0
  20. package/lib/commonjs/components/SecurityBlockedScreen.js.map +1 -0
  21. package/lib/commonjs/components/index.js +13 -0
  22. package/lib/commonjs/components/index.js.map +1 -0
  23. package/lib/commonjs/hooks/index.js +13 -0
  24. package/lib/commonjs/hooks/index.js.map +1 -0
  25. package/lib/commonjs/hooks/useDeviceSecurity.js +81 -0
  26. package/lib/commonjs/hooks/useDeviceSecurity.js.map +1 -0
  27. package/lib/commonjs/index.js +48 -0
  28. package/lib/commonjs/index.js.map +1 -0
  29. package/lib/commonjs/types.js +2 -0
  30. package/lib/commonjs/types.js.map +1 -0
  31. package/lib/module/NativeDeviceSecurity.js +3 -0
  32. package/lib/module/NativeDeviceSecurity.js.map +1 -0
  33. package/lib/module/api.js +206 -0
  34. package/lib/module/api.js.map +1 -0
  35. package/lib/module/components/SecurityBlockedScreen.js +169 -0
  36. package/lib/module/components/SecurityBlockedScreen.js.map +1 -0
  37. package/lib/module/components/index.js +2 -0
  38. package/lib/module/components/index.js.map +1 -0
  39. package/lib/module/hooks/index.js +2 -0
  40. package/lib/module/hooks/index.js.map +1 -0
  41. package/lib/module/hooks/useDeviceSecurity.js +73 -0
  42. package/lib/module/hooks/useDeviceSecurity.js.map +1 -0
  43. package/lib/module/index.js +21 -0
  44. package/lib/module/index.js.map +1 -0
  45. package/lib/module/types.js +2 -0
  46. package/lib/module/types.js.map +1 -0
  47. package/lib/typescript/NativeDeviceSecurity.d.ts +16 -0
  48. package/lib/typescript/NativeDeviceSecurity.d.ts.map +1 -0
  49. package/lib/typescript/api.d.ts +55 -0
  50. package/lib/typescript/api.d.ts.map +1 -0
  51. package/lib/typescript/components/SecurityBlockedScreen.d.ts +23 -0
  52. package/lib/typescript/components/SecurityBlockedScreen.d.ts.map +1 -0
  53. package/lib/typescript/components/index.d.ts +2 -0
  54. package/lib/typescript/components/index.d.ts.map +1 -0
  55. package/lib/typescript/hooks/index.d.ts +3 -0
  56. package/lib/typescript/hooks/index.d.ts.map +1 -0
  57. package/lib/typescript/hooks/useDeviceSecurity.d.ts +7 -0
  58. package/lib/typescript/hooks/useDeviceSecurity.d.ts.map +1 -0
  59. package/lib/typescript/index.d.ts +12 -0
  60. package/lib/typescript/index.d.ts.map +1 -0
  61. package/lib/typescript/types.d.ts +81 -0
  62. package/lib/typescript/types.d.ts.map +1 -0
  63. package/package.json +72 -0
  64. package/react-native-device-security.podspec +18 -0
  65. package/src/NativeDeviceSecurity.ts +33 -0
  66. package/src/api.ts +225 -0
  67. package/src/components/SecurityBlockedScreen.tsx +204 -0
  68. package/src/components/index.ts +1 -0
  69. package/src/hooks/index.ts +5 -0
  70. package/src/hooks/useDeviceSecurity.ts +91 -0
  71. package/src/index.ts +27 -0
  72. package/src/types.ts +95 -0
@@ -0,0 +1,277 @@
1
+ package vn.osp.security
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.*
5
+ import org.json.JSONObject
6
+
7
+ /**
8
+ * React Native module for device security detection
9
+ */
10
+ class DeviceSecurityModule(reactContext: ReactApplicationContext) :
11
+ ReactContextBaseJavaModule(reactContext) {
12
+
13
+ private val rootDetection by lazy { RootDetection(reactContext) }
14
+ private val hookDetection by lazy { HookDetection(reactContext) }
15
+ private val debugDetection by lazy { DebugDetection(reactContext) }
16
+ private val emulatorDetection by lazy { EmulatorDetection(reactContext) }
17
+
18
+ override fun getName(): String = NAME
19
+
20
+ override fun getConstants(): Map<String, Any> {
21
+ val nativeLibLoaded = try {
22
+ NativeSecurityCheck.isNativeLibraryLoaded()
23
+ } catch (e: Exception) {
24
+ Log.e(NAME, "Error checking native library", e)
25
+ false
26
+ }
27
+
28
+ return mapOf(
29
+ "NAME" to NAME,
30
+ "NATIVE_LIBRARY_LOADED" to nativeLibLoaded
31
+ )
32
+ }
33
+
34
+ /**
35
+ * Check if device is rooted (synchronous)
36
+ */
37
+ @ReactMethod(isBlockingSynchronousMethod = true)
38
+ fun isRooted(): Boolean {
39
+ return try {
40
+ val result = rootDetection.performDetection()
41
+ result.isRooted
42
+ } catch (e: Exception) {
43
+ Log.e(NAME, "Error checking root status", e)
44
+ false
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get detailed root detection result
50
+ */
51
+ @ReactMethod
52
+ fun isRootedWithDetails(promise: Promise) {
53
+ try {
54
+ val result = rootDetection.performDetection()
55
+ val json = JSONObject().apply {
56
+ put("isRooted", result.isRooted)
57
+ put("hasRootBeerDetected", result.hasRootBeerDetected)
58
+ put("hasNativeRootDetected", result.hasNativeRootDetected)
59
+ put("hasDangerousBins", result.hasDangerousBins)
60
+ put("hasRootApps", result.hasRootApps)
61
+ put("hasSystemPropsModified", result.hasSystemPropsModified)
62
+ put("details", JSONObject(result.details))
63
+ }
64
+ promise.resolve(json.toString())
65
+ } catch (e: Exception) {
66
+ Log.e(NAME, "Error getting root details", e)
67
+ promise.reject("ROOT_CHECK_ERROR", e.message)
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if Frida is present
73
+ */
74
+ @ReactMethod(isBlockingSynchronousMethod = true)
75
+ fun hasFrida(): Boolean {
76
+ return try {
77
+ hookDetection.hasFrida()
78
+ } catch (e: Exception) {
79
+ Log.e(NAME, "Error checking Frida", e)
80
+ false
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if Xposed is present
86
+ */
87
+ @ReactMethod(isBlockingSynchronousMethod = true)
88
+ fun hasXposed(): Boolean {
89
+ return try {
90
+ hookDetection.hasXposed()
91
+ } catch (e: Exception) {
92
+ Log.e(NAME, "Error checking Xposed", e)
93
+ false
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check if Magisk is present
99
+ */
100
+ @ReactMethod(isBlockingSynchronousMethod = true)
101
+ fun hasMagisk(): Boolean {
102
+ return try {
103
+ hookDetection.hasMagisk()
104
+ } catch (e: Exception) {
105
+ Log.e(NAME, "Error checking Magisk", e)
106
+ false
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Check if app is debuggable
112
+ */
113
+ @ReactMethod(isBlockingSynchronousMethod = true)
114
+ fun isDebuggable(): Boolean {
115
+ return try {
116
+ debugDetection.isDebuggable()
117
+ } catch (e: Exception) {
118
+ Log.e(NAME, "Error checking debuggable", e)
119
+ false
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Check if running on emulator
125
+ */
126
+ @ReactMethod(isBlockingSynchronousMethod = true)
127
+ fun isEmulator(): Boolean {
128
+ return try {
129
+ emulatorDetection.isEmulator()
130
+ } catch (e: Exception) {
131
+ Log.e(NAME, "Error checking emulator", e)
132
+ false
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Get comprehensive security status
138
+ */
139
+ @ReactMethod
140
+ fun getSecurityStatus(promise: Promise) {
141
+ try {
142
+ val rootResult = rootDetection.performDetection()
143
+ val hasFrida = hookDetection.hasFrida()
144
+ val hasXposed = hookDetection.hasXposed()
145
+ val hasMagisk = hookDetection.hasMagisk()
146
+ val isDebuggable = debugDetection.isDebuggable()
147
+ val isEmulator = emulatorDetection.isEmulator()
148
+
149
+ val threats = mutableListOf<String>()
150
+
151
+ if (rootResult.isRooted) {
152
+ threats.add("root_detected")
153
+ if (rootResult.hasRootBeerDetected) threats.add("root_beer_detected")
154
+ if (rootResult.hasNativeRootDetected) threats.add("native_root_detected")
155
+ if (rootResult.hasDangerousBins) threats.add("dangerous_bins_detected")
156
+ if (rootResult.hasRootApps) threats.add("root_apps_detected")
157
+ if (rootResult.hasSystemPropsModified) threats.add("system_props_modified")
158
+ }
159
+ if (hasFrida) threats.add("frida_detected")
160
+ if (hasXposed) threats.add("xposed_detected")
161
+ if (hasMagisk) threats.add("magisk_detected")
162
+ if (isDebuggable) threats.add("debugger_detected")
163
+ if (isEmulator) threats.add("emulator_detected")
164
+
165
+ val securityStatus = JSONObject().apply {
166
+ put("isSecure", threats.isEmpty())
167
+ put("threats", org.json.JSONArray(threats))
168
+ put("isRooted", rootResult.isRooted)
169
+ put("hasRootBeerDetected", rootResult.hasRootBeerDetected)
170
+ put("hasNativeRootDetected", rootResult.hasNativeRootDetected)
171
+ put("hasDangerousBins", rootResult.hasDangerousBins)
172
+ put("hasRootApps", rootResult.hasRootApps)
173
+ put("hasSystemPropsModified", rootResult.hasSystemPropsModified)
174
+ put("hasFrida", hasFrida)
175
+ put("hasXposed", hasXposed)
176
+ put("hasMagisk", hasMagisk)
177
+ put("isDebuggable", isDebuggable)
178
+ put("isEmulator", isEmulator)
179
+ put("details", JSONObject().apply {
180
+ put("emulatorType", emulatorDetection.getEmulatorType())
181
+ val nativeLibLoaded = try {
182
+ NativeSecurityCheck.isNativeLibraryLoaded()
183
+ } catch (e: Exception) {
184
+ false
185
+ }
186
+ put("nativeLibraryLoaded", nativeLibLoaded)
187
+ })
188
+ }
189
+
190
+ promise.resolve(securityStatus.toString())
191
+ } catch (e: Exception) {
192
+ Log.e(NAME, "Error getting security status", e)
193
+ promise.reject("SECURITY_STATUS_ERROR", e.message)
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Check if device is secure (no threats)
199
+ */
200
+ @ReactMethod
201
+ fun isDeviceSecure(promise: Promise) {
202
+ try {
203
+ val rootResult = rootDetection.performDetection()
204
+ val isSecure = !rootResult.isRooted &&
205
+ !hookDetection.hasFrida() &&
206
+ !hookDetection.hasXposed() &&
207
+ !hookDetection.hasMagisk() &&
208
+ !debugDetection.isDebuggable() &&
209
+ !emulatorDetection.isEmulator()
210
+
211
+ promise.resolve(isSecure)
212
+ } catch (e: Exception) {
213
+ Log.e(NAME, "Error checking device security", e)
214
+ promise.reject("SECURITY_CHECK_ERROR", e.message)
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Block app when security threat detected
220
+ * Shows an alert and exits the app
221
+ */
222
+ @ReactMethod
223
+ fun blockOnSecurityThreat(
224
+ showAlert: Boolean,
225
+ alertTitle: String,
226
+ alertMessage: String,
227
+ alertButtonText: String
228
+ ) {
229
+ try {
230
+ val rootResult = rootDetection.performDetection()
231
+ val hasFrida = hookDetection.hasFrida()
232
+ val hasXposed = hookDetection.hasXposed()
233
+ val hasMagisk = hookDetection.hasMagisk()
234
+ val isDebuggable = debugDetection.isDebuggable()
235
+ val isEmulator = emulatorDetection.isEmulator()
236
+
237
+ val hasThreat = rootResult.isRooted ||
238
+ hasFrida ||
239
+ hasXposed ||
240
+ hasMagisk ||
241
+ isDebuggable ||
242
+ isEmulator
243
+
244
+ if (hasThreat) {
245
+ Log.w(NAME, "Security threat detected, blocking app")
246
+
247
+ // Show alert if requested
248
+ if (showAlert) {
249
+ val activity = currentActivity
250
+ activity?.runOnUiThread {
251
+ android.app.AlertDialog.Builder(activity)
252
+ .setCancelable(false)
253
+ .setTitle(alertTitle)
254
+ .setMessage(alertMessage)
255
+ .setPositiveButton(alertButtonText) { _, _ ->
256
+ activity.finishAffinity()
257
+ System.exit(0)
258
+ }
259
+ .show()
260
+ }
261
+ } else {
262
+ // Exit without alert
263
+ currentActivity?.finishAffinity()
264
+ System.exit(0)
265
+ }
266
+ } else {
267
+ Log.i(NAME, "No security threat detected, app continues")
268
+ }
269
+ } catch (e: Exception) {
270
+ Log.e(NAME, "Error blocking on security threat", e)
271
+ }
272
+ }
273
+
274
+ companion object {
275
+ const val NAME = "DeviceSecurity"
276
+ }
277
+ }
@@ -0,0 +1,58 @@
1
+ package vn.osp.security
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ /**
11
+ * React Native package for device security module
12
+ */
13
+ class DeviceSecurityPackage : ReactPackage {
14
+
15
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
16
+ return listOf(DeviceSecurityModule(reactContext))
17
+ }
18
+
19
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
20
+ return emptyList()
21
+ }
22
+
23
+ override fun getModuleInfoProvider(): ReactModuleInfoProvider {
24
+ return ReactModuleInfoProvider {
25
+ val moduleInfo: MutableMap<String, ReactModuleInfo> = java.util.HashMap()
26
+
27
+ val methods = listOf(
28
+ "isRooted",
29
+ "isRootedWithDetails",
30
+ "hasFrida",
31
+ "hasXposed",
32
+ "hasMagisk",
33
+ "isDebuggable",
34
+ "isEmulator",
35
+ "getSecurityStatus",
36
+ "isDeviceSecure",
37
+ "blockOnSecurityThreat"
38
+ )
39
+
40
+ val constants = listOf("NAME", "NATIVE_LIBRARY_LOADED")
41
+
42
+ moduleInfo["DeviceSecurity"] = ReactModuleInfo(
43
+ "DeviceSecurity",
44
+ "DeviceSecurity",
45
+ false, // isTurboModule
46
+ false, // isCxxModule
47
+ true, // canOverrideExistingModule
48
+ methods,
49
+ constants,
50
+ false, // supportsEventEmitter
51
+ false, // needsDispatchEvent
52
+ true // hasConstants
53
+ )
54
+
55
+ moduleInfo
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,204 @@
1
+ package vn.osp.security
2
+
3
+ import android.content.Context
4
+ import android.content.pm.PackageManager
5
+ import android.os.Build
6
+ import android.telephony.TelephonyManager
7
+ import java.io.File
8
+
9
+ /**
10
+ * Detection for Android emulators
11
+ */
12
+ class EmulatorDetection(private val context: Context) {
13
+
14
+ /**
15
+ * Check if running on emulator
16
+ */
17
+ fun isEmulator(): Boolean {
18
+ return checkKnownEmulatorProperties() ||
19
+ checkEmulatorBuildProps() ||
20
+ checkEmulatorFiles() ||
21
+ checkEmulatorFeatures() ||
22
+ checkNetworkInterfaces() ||
23
+ checkCamera()
24
+ }
25
+
26
+ /**
27
+ * Check for known emulator properties
28
+ */
29
+ private fun checkKnownEmulatorProperties(): Boolean {
30
+ val emulatorProps = listOf(
31
+ "generic" to Build.BRAND,
32
+ "generic" to Build.PRODUCT,
33
+ "google_sdk" to Build.PRODUCT,
34
+ "sdk" to Build.PRODUCT,
35
+ "sdk_gphone" to Build.PRODUCT,
36
+ "sdk_gphone64_arm64" to Build.PRODUCT,
37
+ "sdk_gphone_x86" to Build.PRODUCT,
38
+ "sdk_gphone64_x86_64" to Build.PRODUCT,
39
+ "vbox86p" to Build.HARDWARE,
40
+ "vmos" to Build.HARDWARE,
41
+ "nox" to Build.HARDWARE,
42
+ "ttVM_x86" to Build.MANUFACTURER,
43
+ "Genymotion" to Build.MANUFACTURER,
44
+ "Genymotion" to Build.PRODUCT,
45
+ "Android SDK built for x86" to Build.MANUFACTURER,
46
+ "Android SDK built for x86_64" to Build.MANUFACTURER
47
+ )
48
+
49
+ for ((key, value) in emulatorProps) {
50
+ if (value.contains(key, ignoreCase = true)) {
51
+ return true
52
+ }
53
+ }
54
+
55
+ return false
56
+ }
57
+
58
+ /**
59
+ * Check emulator build properties
60
+ */
61
+ private fun checkEmulatorBuildProps(): Boolean {
62
+ // Check device model
63
+ val model = Build.MODEL.lowercase()
64
+ if (model.contains("sdk") ||
65
+ model.contains("google_sdk") ||
66
+ model.contains("emulator") ||
67
+ model.contains("android sdk")) {
68
+ return true
69
+ }
70
+
71
+ // Check device
72
+ val device = Build.DEVICE.lowercase()
73
+ if (device.contains("generic") ||
74
+ device.contains("emulator") ||
75
+ device.contains("sdk")) {
76
+ return true
77
+ }
78
+
79
+ // Check fingerprint
80
+ val fingerprint = Build.FINGERPRINT.lowercase()
81
+ if (fingerprint.contains("generic") ||
82
+ fingerprint.contains("sdk_gphone")) {
83
+ return true
84
+ }
85
+
86
+ // Check hardware
87
+ val hardware = Build.HARDWARE.lowercase()
88
+ if (hardware.contains("goldfish") ||
89
+ hardware.contains("ranchu") ||
90
+ hardware.contains("vbox")) {
91
+ return true
92
+ }
93
+
94
+ return false
95
+ }
96
+
97
+ /**
98
+ * Check for emulator-specific files
99
+ */
100
+ private fun checkEmulatorFiles(): Boolean {
101
+ val emulatorPaths = listOf(
102
+ "/dev/socket/qemud",
103
+ "/dev/qemu_pipe",
104
+ "/system/lib/libc_malloc_debug_qemu.so",
105
+ "/system/bin/qemu-props",
106
+ "/system/lib/libc_malloc_debug_qemu.so",
107
+ "/dev/socket/genyd",
108
+ "/dev/socket/baseband_genyd",
109
+ "/system/lib/libnim-dumo.so",
110
+ "/system/lib/libdum.so"
111
+ )
112
+
113
+ return emulatorPaths.any { File(it).exists() }
114
+ }
115
+
116
+ /**
117
+ * Check for emulator features
118
+ */
119
+ private fun checkEmulatorFeatures(): Boolean {
120
+ val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
121
+ ?: return false
122
+
123
+ // Check for invalid network operator
124
+ val networkOperator = telephonyManager.networkOperator
125
+ if (networkOperator == null || networkOperator == "000000000000000") {
126
+ return true
127
+ }
128
+
129
+ // Check for invalid subscriber ID
130
+ val subscriberId = telephonyManager.subscriberId
131
+ if (subscriberId == null || subscriberId == "000000000000000") {
132
+ return true
133
+ }
134
+
135
+ // Check for invalid IMEI/MEID
136
+ val deviceId = telephonyManager.deviceId
137
+ if (deviceId == null || deviceId == "000000000000000" || deviceId == "0") {
138
+ return true
139
+ }
140
+
141
+ return false
142
+ }
143
+
144
+ /**
145
+ * Check network interfaces for emulator indicators
146
+ */
147
+ private fun checkNetworkInterfaces(): Boolean {
148
+ val emulatorInterfaces = listOf(
149
+ "vboxnet",
150
+ "vnic",
151
+ "tun",
152
+ "tap"
153
+ )
154
+
155
+ try {
156
+ val interfaces = java.net.NetworkInterface.getNetworkInterfaces()
157
+ for (networkInterface in interfaces.toList()) {
158
+ val name = networkInterface.name.lowercase()
159
+ for (emulatorInterface in emulatorInterfaces) {
160
+ if (name.contains(emulatorInterface)) {
161
+ return true
162
+ }
163
+ }
164
+ }
165
+ } catch (e: Exception) {
166
+ // Ignore errors
167
+ }
168
+
169
+ return false
170
+ }
171
+
172
+ /**
173
+ * Check camera availability (emulators often lack cameras)
174
+ * Note: This is just a hint, many real devices also lack cameras
175
+ */
176
+ private fun checkCamera(): Boolean {
177
+ return try {
178
+ // Check if device has camera hardware feature
179
+ val hasCamera = context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
180
+ || context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)
181
+
182
+ // If no camera feature, might be emulator (but not guaranteed)
183
+ !hasCamera
184
+ } catch (e: Exception) {
185
+ // On error, assume it's not an emulator based on camera check
186
+ false
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Get emulator type if detected
192
+ */
193
+ fun getEmulatorType(): String? {
194
+ return when {
195
+ Build.HARDWARE.contains("ranchu") -> "QEMU/KVM"
196
+ Build.HARDWARE.contains("vbox") -> "VirtualBox"
197
+ Build.MANUFACTURER.contains("Genymotion") -> "Genymotion"
198
+ Build.PRODUCT.contains("nox") -> "Nox"
199
+ Build.HARDWARE.contains("vmos") -> "VMOS"
200
+ Build.BRAND.contains("google") && Build.PRODUCT.contains("sdk") -> "Android Emulator"
201
+ else -> "Unknown"
202
+ }
203
+ }
204
+ }