From ef08cf2427cdbebede341a8ff6c3ec1ea4e0c32b Mon Sep 17 00:00:00 2001 From: koalasat Date: Wed, 16 Jul 2025 19:13:21 +0200 Subject: [PATCH] Security checks --- frontend/src/contexts/AppContext.tsx | 2 +- .../main/java/com/robosats/MainActivity.kt | 311 ++++++++++++++---- .../main/java/com/robosats/WebAppInterface.kt | 205 ++++++++++-- 3 files changed, 426 insertions(+), 92 deletions(-) diff --git a/frontend/src/contexts/AppContext.tsx b/frontend/src/contexts/AppContext.tsx index a1c39ffc..e23e25c6 100644 --- a/frontend/src/contexts/AppContext.tsx +++ b/frontend/src/contexts/AppContext.tsx @@ -161,7 +161,7 @@ export interface UseAppStoreType { export const initialAppContext: UseAppStoreType = { theme: undefined, - torStatus: 'STARTING', + torStatus: 'ON', settings: getSettings(), setSettings: () => {}, page: entryPage, diff --git a/mobile_new/app/src/main/java/com/robosats/MainActivity.kt b/mobile_new/app/src/main/java/com/robosats/MainActivity.kt index e45800e6..86323139 100644 --- a/mobile_new/app/src/main/java/com/robosats/MainActivity.kt +++ b/mobile_new/app/src/main/java/com/robosats/MainActivity.kt @@ -1,17 +1,29 @@ package com.robosats +import android.annotation.SuppressLint import android.app.Application import android.content.Context import android.graphics.Bitmap +import android.os.Build import android.os.Bundle import android.util.Log import android.view.View -import android.webkit.* +import android.webkit.ConsoleMessage +import android.webkit.CookieManager +import android.webkit.GeolocationPermissions +import android.webkit.PermissionRequest +import android.webkit.ServiceWorkerController +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebStorage +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.TextView -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout -import com.robosats.R import com.robosats.tor.TorKmp import com.robosats.tor.TorKmpManager import java.io.ByteArrayInputStream @@ -26,6 +38,13 @@ class MainActivity : AppCompatActivity() { private lateinit var loadingContainer: ConstraintLayout private lateinit var statusTextView: TextView + // Security constants + private val ALLOWED_DOMAINS = arrayOf(".onion") + private val CONTENT_SECURITY_POLICY = "default-src 'self'; connect-src 'self' https://*.onion http://*.onion; " + + "script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " + + "img-src 'self' data:; font-src 'self' data:; object-src 'none'; " + + "media-src 'none'; frame-src 'none'; worker-src 'self';" + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -152,40 +171,8 @@ class MainActivity : AppCompatActivity() { } } - // Configure WebView settings on UI thread - val webSettings = webView.settings - - // Enable JavaScript - webSettings.javaScriptEnabled = true - - // Enable DOM storage for HTML5 apps - webSettings.domStorageEnabled = true - - // Enable CORS and cross-origin requests - webSettings.allowUniversalAccessFromFileURLs = true - webSettings.allowFileAccessFromFileURLs = true - - // Disable cache completely to prevent leaks - webSettings.cacheMode = WebSettings.LOAD_NO_CACHE - - // Enable mixed content (http in https) - webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW - - // Enable zooming - webSettings.setSupportZoom(true) - webSettings.builtInZoomControls = true - webSettings.displayZoomControls = false - - // Enable HTML5 features - webSettings.allowFileAccess = true - webSettings.allowContentAccess = true - webSettings.loadWithOverviewMode = true - webSettings.useWideViewPort = true - webSettings.setSupportMultipleWindows(true) - webSettings.javaScriptCanOpenWindowsAutomatically = true - - // Improve display for better Android integration - webSettings.textZoom = 100 // Normal text zoom + // Configure WebView settings on UI thread with security as priority + secureWebViewSettings() // Show message that we're setting up secure browsing runOnUiThread { @@ -222,6 +209,30 @@ class MainActivity : AppCompatActivity() { runOnUiThread { updateStatus("Secure connection established. Loading app...") + // Set up WebChromeClient with restricted permissions + webView.webChromeClient = object : WebChromeClient() { + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + // Deny all geolocation requests + callback.invoke(origin, false, false) + Log.d("SecurityPolicy", "Blocked geolocation request from: $origin") + } + + override fun onPermissionRequest(request: PermissionRequest) { + // Deny all permission requests from web content + request.deny() + Log.d("SecurityPolicy", "Denied permission request: ${request.resources.joinToString()}") + } + + // Control console messages + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + Log.d("WebViewConsole", "${consoleMessage.message()} -- From line ${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}") + return true + } + } + // Create a custom WebViewClient that forces all traffic through Tor webView.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { @@ -234,10 +245,22 @@ class MainActivity : AppCompatActivity() { val urlString = request.url.toString() Log.d("TorProxy", "Intercepting request: $urlString") + // Block all external requests that aren't to .onion domains or local files + if (!isAllowedRequest(urlString)) { + Log.e("SecurityPolicy", "Blocked forbidden request to: $urlString") + return WebResourceResponse("text/plain", "UTF-8", null) + } + try { // Special handling for .onion domains val isOnionDomain = urlString.contains(".onion") + // Only proceed if it's an onion domain or local file + if (!isOnionDomain && !urlString.startsWith("file://")) { + Log.e("SecurityPolicy", "Blocked non-onion external request: $urlString") + return WebResourceResponse("text/plain", "UTF-8", null) + } + // For .onion domains, we must use SOCKS proxy type val proxyType = if (isOnionDomain) Proxy.Type.SOCKS @@ -254,6 +277,12 @@ class MainActivity : AppCompatActivity() { Log.d("TorProxy", "Handling .onion domain with SOCKS proxy: $urlString") } + // If it's a local file, return it directly + if (urlString.startsWith("file://")) { + // Let the system handle local files + return super.shouldInterceptRequest(view, request) + } + // Create connection with proxy already configured val url = URL(urlString) val connection = url.openConnection(torProxy) @@ -266,22 +295,46 @@ class MainActivity : AppCompatActivity() { // Ensure no connection reuse to prevent proxy leaks connection.setRequestProperty("Connection", "close") + // Add security headers + connection.setRequestProperty("Sec-Fetch-Site", "same-origin") + connection.setRequestProperty("Sec-Fetch-Mode", "cors") + connection.setRequestProperty("DNT", "1") // Do Not Track + // Copy request headers request.requestHeaders.forEach { (key, value) -> connection.setRequestProperty(key, value) } + // Set the request method + connection.requestMethod = request.method + // Special handling for OPTIONS (CORS preflight) requests if (request.method == "OPTIONS") { - // Handle preflight CORS request - connection.requestMethod = "OPTIONS" - connection.setRequestProperty("Access-Control-Request-Method", - request.requestHeaders["Access-Control-Request-Method"] ?: "GET, POST, OPTIONS") - connection.setRequestProperty("Access-Control-Request-Headers", - request.requestHeaders["Access-Control-Request-Headers"] ?: "") - } else { - // Set request method for non-OPTIONS requests - connection.requestMethod = request.method + // For OPTIONS, we'll create a custom response without making a network request + // This is the most reliable way to handle CORS preflight + Log.d("CORS", "Handling OPTIONS preflight request for: $urlString") + + // Create CORS headers map + val preflightHeaders = HashMap() + preflightHeaders["Access-Control-Allow-Origin"] = "*" + preflightHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE, HEAD" + preflightHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization" + preflightHeaders["Access-Control-Max-Age"] = "86400" // Cache preflight for 24 hours + preflightHeaders["Access-Control-Allow-Credentials"] = "true" + preflightHeaders["Content-Type"] = "text/plain" + + // Log CORS headers for debugging + Log.d("CORS", "Preflight response with CORS headers: $preflightHeaders") + + // Return a custom preflight response without actually connecting + return WebResourceResponse( + "text/plain", + "UTF-8", + 200, + "OK", + preflightHeaders, + ByteArrayInputStream("".toByteArray()) + ) } // Try to connect @@ -301,27 +354,50 @@ class MainActivity : AppCompatActivity() { connection.inputStream } - // Create response headers map with CORS headers + // Create response headers map with security headers val responseHeaders = HashMap() - // Add CORS headers - responseHeaders["Access-Control-Allow-Origin"] = "*" - responseHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" - responseHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept" - responseHeaders["Access-Control-Allow-Credentials"] = "true" - - // Copy original response headers + // First copy original response headers, but carefully handle CORS headers for (i in 0 until connection.headerFields.size) { val key = connection.headerFields.keys.elementAtOrNull(i) - if (key != null) { - val value = connection.getHeaderField(key) - if (value != null) { - responseHeaders[key] = value + if (key != null && key.isNotEmpty()) { + // Skip any CORS headers from the original response - we'll add our own + if (!key.startsWith("Access-Control-")) { + val value = connection.getHeaderField(key) + if (value != null) { + responseHeaders[key] = value + } + } else { + // Log any CORS headers we're skipping from the original response + Log.d("CORS", "Skipping original CORS header: $key: ${connection.getHeaderField(key)}") } } } - // Return proxied response with CORS headers + // Add our own CORS headers + responseHeaders["Access-Control-Allow-Origin"] = "*" + responseHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE, HEAD" + responseHeaders["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization" + responseHeaders["Access-Control-Allow-Credentials"] = "true" + if (!responseHeaders.containsKey("Content-Security-Policy")) { + responseHeaders["Content-Security-Policy"] = CONTENT_SECURITY_POLICY + } + if (!responseHeaders.containsKey("X-Content-Type-Options")) { + responseHeaders["X-Content-Type-Options"] = "nosniff" + } + if (!responseHeaders.containsKey("X-Frame-Options")) { + responseHeaders["X-Frame-Options"] = "DENY" + } + if (!responseHeaders.containsKey("Referrer-Policy")) { + responseHeaders["Referrer-Policy"] = "no-referrer" + } + + // Log the CORS headers for debugging + responseHeaders["Access-Control-Allow-Origin"]?.let { + Log.d("CORS", "Access-Control-Allow-Origin: $it") + } + + // Return proxied response with security headers return WebResourceResponse( mimeType, encoding, @@ -343,11 +419,13 @@ class MainActivity : AppCompatActivity() { } catch (e: Exception) { Log.e("TorProxy", "Error proxying request: $urlString - ${e.message}", e) - // For non-onion domains, let the system handle it - return super.shouldInterceptRequest(view, request) + // For security, block the request rather than falling back to system handling + return WebResourceResponse("text/plain", "UTF-8", null) } } + // We're not handling SSL, so we don't need the onReceivedSslError method + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { // Verify Tor is still connected before allowing any request if (!torKmp.isConnected()) { @@ -450,6 +528,117 @@ class MainActivity : AppCompatActivity() { /** * Sets the proxy for WebView using the most direct approach that's known to work with Tor */ + /** + * Configure WebView settings with a security-first approach + */ + @SuppressLint("SetJavaScriptEnabled") + private fun secureWebViewSettings() { + val webSettings = webView.settings + + // --- SECURITY SETTINGS --- + + // 1. JavaScript is required for the app to function, but we restrict it + webSettings.javaScriptEnabled = true // Required, but we'll restrict its capabilities + + // 2. Disable features that could lead to data leakage + webSettings.saveFormData = false + webSettings.savePassword = false + webSettings.cacheMode = WebSettings.LOAD_NO_CACHE + webSettings.setGeolocationEnabled(false) + + // 3. Disable database access + webSettings.databaseEnabled = false + webSettings.domStorageEnabled = true // Required for most modern web apps + + // 4. File access settings - must allow cross-origin access for our use case + webSettings.allowFileAccess = true // Needed for loading internal HTML + webSettings.allowContentAccess = false + webSettings.allowFileAccessFromFileURLs = true // Required for local HTML to work + webSettings.allowUniversalAccessFromFileURLs = true // Required to allow CORS from file:// to onion URLs + + // Log these critical settings for debugging + Log.d("WebViewSettings", "allowFileAccessFromFileURLs: ${webSettings.allowFileAccessFromFileURLs}") + Log.d("WebViewSettings", "allowUniversalAccessFromFileURLs: ${webSettings.allowUniversalAccessFromFileURLs}") + + // 5. Disable potentially risky features + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + webSettings.javaScriptCanOpenWindowsAutomatically = false + } + webSettings.setSupportMultipleWindows(false) + + // 6. Set secure mixed content mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW + } + + // 7. Disable plugins (none needed for our app) + webSettings.pluginState = WebSettings.PluginState.OFF + + // 8. Configure cookies for security + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptCookie(true) // We need cookies for the app to function + cookieManager.setAcceptThirdPartyCookies(webView, false) // Block 3rd party cookies + + // 10. Disable Service Workers (not needed for our local app) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + ServiceWorkerController.getInstance().setServiceWorkerClient(null) + } + + // --- USABILITY SETTINGS --- + + // Allow zooming for better accessibility + webSettings.setSupportZoom(true) + webSettings.builtInZoomControls = true + webSettings.displayZoomControls = false + + // Improve display for better Android integration + webSettings.loadWithOverviewMode = true + webSettings.useWideViewPort = true + webSettings.textZoom = 100 + } + + /** + * Check if a URL request is allowed based on security policy + */ + private fun isAllowedRequest(url: String): Boolean { + // Always allow local file requests + if (url.startsWith("file:///android_asset/") || url.startsWith("file:///data/")) { + return true + } + + // Allow onion domains + if (ALLOWED_DOMAINS.any { url.contains(it) }) { + return true + } + + // Block everything else + return false + } + + // SSL error description method removed as we're not using SSL + + /** + * Clear all WebView data when activity is destroyed + */ + override fun onDestroy() { + // Clear all cookies, cache, and WebView data for privacy + CookieManager.getInstance().removeAllCookies(null) + CookieManager.getInstance().flush() + + webView.clearCache(true) + webView.clearHistory() + webView.clearFormData() + webView.clearSslPreferences() + + WebStorage.getInstance().deleteAllData() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().removeSessionCookies(null) + } + + super.onDestroy() + } + private fun setWebViewProxy(context: Context, proxyHost: String, proxyPort: Int) { try { // First set system properties (required as a foundation) diff --git a/mobile_new/app/src/main/java/com/robosats/WebAppInterface.kt b/mobile_new/app/src/main/java/com/robosats/WebAppInterface.kt index 59dd09f5..3da7e3b4 100644 --- a/mobile_new/app/src/main/java/com/robosats/WebAppInterface.kt +++ b/mobile_new/app/src/main/java/com/robosats/WebAppInterface.kt @@ -6,11 +6,28 @@ import android.webkit.JavascriptInterface import android.webkit.WebView import android.widget.Toast import com.robosats.tor.TorKmpManager.getTorKmpObject +import android.annotation.SuppressLint +import android.text.TextUtils +import java.util.UUID +import java.util.regex.Pattern +/** + * Provides a secure bridge between JavaScript and native Android code. + * This class is designed with security in mind, implementing input validation, + * sanitization, and proper error handling. + */ +@SuppressLint("SetJavaScriptEnabled") class WebAppInterface(private val context: Context, private val webView: WebView) { private val TAG = "WebAppInterface" private val roboIdentities = RoboIdentities() + // Security patterns for input validation + private val UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE) + private val SAFE_STRING_PATTERN = Pattern.compile("^[a-zA-Z0-9\\s_\\-.,:;!?()\\[\\]{}]*$") + + // Maximum length for input strings + private val MAX_INPUT_LENGTH = 1000 + init { // Check if libraries are loaded and show a toast notification if there's an issue if (!RoboIdentities.areLibrariesLoaded()) { @@ -23,70 +40,198 @@ class WebAppInterface(private val context: Context, private val webView: WebView } } + /** + * Validates that a string contains only safe characters and is within length limits + */ + private fun isValidInput(input: String?, maxLength: Int = MAX_INPUT_LENGTH): Boolean { + if (input == null || input.isEmpty() || input.length > maxLength) { + return false + } + return SAFE_STRING_PATTERN.matcher(input).matches() + } + + /** + * Validates that a string is a valid UUID + */ + private fun isValidUuid(uuid: String?): Boolean { + if (uuid == null || uuid.isEmpty()) { + return false + } + return UUID_PATTERN.matcher(uuid).matches() + } + + /** + * Safely evaluates JavaScript, escaping any potentially dangerous characters + */ + private fun safeEvaluateJavascript(script: String) { + // Remove any null bytes which could be used to trick the JS interpreter + val sanitizedScript = script.replace("\u0000", "") + + webView.post { + try { + webView.evaluateJavascript(sanitizedScript, null) + } catch (e: Exception) { + Log.e(TAG, "Error evaluating JavaScript: $e") + } + } + } + + /** + * Safely encodes a string for use in JavaScript + */ + private fun encodeForJavaScript(input: String): String { + return input.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("<", "\\u003C") + .replace(">", "\\u003E") + .replace("&", "\\u0026") + } + + /** + * Generate a robot name from the given message + * @param uuid A unique identifier for the JavaScript Promise + * @param message The input message to generate a robot name from + */ @JavascriptInterface fun generateRoboname(uuid: String, message: String) { + // Validate inputs before processing + if (!isValidUuid(uuid) || !isValidInput(message)) { + Log.e(TAG, "Invalid input for generateRoboname: uuid=$uuid, message=$message") + rejectPromise(uuid, "Invalid input parameters") + return + } + try { - val roboname = roboIdentities.generateRoboname(message) - webView.post { - webView.evaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('${uuid}', '${roboname}')", null) - } + // Sanitize the input before passing to native code + val sanitizedMessage = message.trim() + + // Generate the roboname + val roboname = roboIdentities.generateRoboname(sanitizedMessage) + + // Safely encode and return the result + resolvePromise(uuid, roboname ?: "") } catch (e: Exception) { Log.e(TAG, "Error in generateRoboname", e) - - // Handle error gracefully by returning a fallback value - webView.post { - webView.evaluateJavascript( - "javascript:window.AndroidRobosats.onRejectPromise('${uuid}', 'Error generating robot name')", - null - ) - } + rejectPromise(uuid, "Error generating robot name") } } + /** + * Helper function to safely resolve a JavaScript Promise + */ + private fun resolvePromise(uuid: String, result: String) { + if (!isValidUuid(uuid)) { + Log.e(TAG, "Invalid UUID for promise resolution: $uuid") + return + } + + val encodedResult = encodeForJavaScript(result) + safeEvaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('$uuid', '$encodedResult')") + } + + /** + * Helper function to safely reject a JavaScript Promise + */ + private fun rejectPromise(uuid: String, errorMessage: String) { + if (!isValidUuid(uuid)) { + Log.e(TAG, "Invalid UUID for promise rejection: $uuid") + return + } + + val encodedError = encodeForJavaScript(errorMessage) + safeEvaluateJavascript("javascript:window.AndroidRobosats.onRejectPromise('$uuid', '$encodedError')") + } + + /** + * Generate a robot hash from the given message + * @param uuid A unique identifier for the JavaScript Promise + * @param message The input message to generate a robot hash from + */ @JavascriptInterface fun generateRobohash(uuid: String, message: String) { + // Validate inputs before processing + if (!isValidUuid(uuid) || !isValidInput(message)) { + Log.e(TAG, "Invalid input for generateRobohash: uuid=$uuid, message=$message") + rejectPromise(uuid, "Invalid input parameters") + return + } + try { - val roboname = roboIdentities.generateRobohash(message) - webView.post { - webView.evaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('${uuid}', '${roboname}')", null) - } + // Sanitize the input before passing to native code + val sanitizedMessage = message.trim() + + // Generate the robohash + val robohash = roboIdentities.generateRobohash(sanitizedMessage) + + // Safely encode and return the result + resolvePromise(uuid, robohash ?: "") } catch (e: Exception) { Log.e(TAG, "Error in generateRobohash", e) - - // Handle error gracefully by returning a fallback value - webView.post { - webView.evaluateJavascript( - "javascript:window.AndroidRobosats.onRejectPromise('${uuid}', 'Error generating robot hash')", - null - ) - } + rejectPromise(uuid, "Error generating robot hash") } } + /** + * Copy text to the clipboard + * @param message The text to copy to the clipboard + */ @JavascriptInterface fun copyToClipboard(message: String) { + // Validate input + if (!isValidInput(message, 10000)) { // Allow longer text for clipboard + Log.e(TAG, "Invalid input for copyToClipboard") + Toast.makeText(context, "Invalid content for clipboard", Toast.LENGTH_SHORT).show() + return + } + try { + // Limit clipboard content size for security + val truncatedMessage = if (message.length > 10000) { + message.substring(0, 10000) + "... (content truncated for security)" + } else { + message + } + + // Copy to clipboard val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - val clip = android.content.ClipData.newPlainText("RoboSats Data", message) + val clip = android.content.ClipData.newPlainText("RoboSats Data", truncatedMessage) clipboard.setPrimaryClip(clip) // Show a toast notification Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() - // Log the action - Log.d(TAG, "Text copied to clipboard") + // Log the action (don't log the content for privacy) + Log.d(TAG, "Text copied to clipboard (${truncatedMessage.length} chars)") } catch (e: Exception) { Log.e(TAG, "Error copying to clipboard", e) Toast.makeText(context, "Failed to copy to clipboard", Toast.LENGTH_SHORT).show() } } + /** + * Get the current Tor connection status + * @param uuid A unique identifier for the JavaScript Promise + */ @JavascriptInterface fun getTorStatus(uuid: String) { - val torState = getTorKmpObject().torState.state.name + // Validate UUID + if (!isValidUuid(uuid)) { + Log.e(TAG, "Invalid UUID for getTorStatus: $uuid") + return + } - webView.post { - webView.evaluateJavascript("javascript:window.AndroidRobosats.onResolvePromise('${uuid}', '${torState}')", null) + try { + // Get Tor status safely + val torState = getTorKmpObject().torState.state.name + + // Return the status through the secure promise resolution + resolvePromise(uuid, torState) + } catch (e: Exception) { + Log.e(TAG, "Error getting Tor status", e) + rejectPromise(uuid, "Error retrieving Tor status") } } }