Wi-Fi 和許多其他縮寫。 如何在Android應用程式中獲取Wi-Fi節點的資料而不腫

有一天,我需要從 Android 應用程式掃描 Wi-Fi 網路並獲取有關接入點的詳細數據。

在這裡我們不得不面對幾個困難: off.Android 文檔 許多描述的類別已被棄用(API等級> 26),這並沒有反映在其中; 文件中對某些內容的描述很少(例如,類別的功能字段 掃描結果 在撰寫本文時,幾乎沒有任何描述,儘管它包含許多重要數據)。 第三個困難可能在於,當你第一次接觸Wi-Fi時,除了閱讀理論和透過本地主機設定路由器之外,你還必須處理許多看起來可以單獨理解的縮寫。 但如何連結和建構它們可能並不明顯(判斷是主觀的,取決於先前的經驗)。

本文討論如何從 Android 程式碼中獲取有關 Wi-Fi 環境的全面數據,無需 NDK、hacks,而僅使用 Android API 並了解如何解釋它。

讓我們事不宜遲,開始寫程式碼吧。

1. 建立項目

本說明適用於那些多次建立 Android 專案的人,因此我們省略了此項目的詳細資訊。 下面的程式碼將會在 Kotlin 中呈現,minSdkVersion=23。

2. 存取權限

要從應用程式使用 Wi-Fi,您需要獲得使用者的多項權限。 依據 文檔,為了在8.0以後的作業系統版本的設備上掃描網絡,除了具有查看網絡環境狀態的權限外,還需要具有更改設備Wi-Fi模組狀態的權限,或者需要具有坐標(大約或準確)。 從版本 9.0 開始,您必須提示使用者兩者,並明確要求使用者開啟位置服務。 不要忘記勇敢地向用戶解釋這是Google的突發奇想,而不是我們想要監視他:)

因此,在 AndroidManifest.xml 中我們將會加入:

    <uses-permission android_name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android_name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android_name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android_name="android.permission.ACCESS_FINE_LOCATION"/>

在包含目前活動連結的程式碼中:

import android.app.Activity
import android.content.Context
import android.location.LocationManager
import androidx.core.app.ActivityCompat

....

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CHANGE_WIFI_STATE),
                1
            )
            makeEnableLocationServices(activity.applicationContext)
        } else {
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(Manifest.permission.CHANGE_WIFI_STATE),
                1
            )
        }

    /* включает экран включения службы по определению местоположения */
    fun makeEnableLocationServices(context: Context) {
        // TODO: перед вызовом этой функции надо рассказать пользователю, зачем Вам доступ к местоположению
        val lm: LocationManager =
            context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager

        val gpsEnabled: Boolean = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
        val networkEnabled: Boolean = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER);

        if (!gpsEnabled && !networkEnabled) {
            context.startActivity(Intent(ACTION_LOCATION_SOURCE_SETTINGS));
        }
    }

3.創建BroadcastReceiver,訂閱掃描Wi-Fi網路環境的資料更新事件

val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager

val wifiScanReceiver = object : BroadcastReceiver() {

  override fun onReceive(context: Context, intent: Intent) {
    val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
    if (success) {
      scanSuccess()
    } 
  }
}

val intentFilter = IntentFilter()
/* подписываемся на сообщения о получении новых результатов сканирования */
intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
context.registerReceiver(wifiScanReceiver, intentFilter)

val success = wifiManager.startScan()
if (!success) {
  /* что-то не получилось при запуске сканирования, проверьте выданые разрешения */
}

....

private fun scanSuccess() {
 /* вот они, результаты сканирования */
  val results: List<ScanResult> = wifiManager.scanResults
}

文件中的 WiFiManager.startScan 方法自 API 版本 28 起標示為已棄用,但已關閉。 說明 建議使用它。

總共,我們收到了一個物件列表 掃描結果.

4.查看ScanResult並理解術語

讓我們來看看這個類別的一些欄位並描述它們的含義:

SSID — 服務集識別碼是網路的名稱

BSSID – 基本服務集識別碼 – 網路介面卡(Wi-Fi 熱點)的 MAC 位址

水平 — 接收訊號強度指示器 [dBm(俄羅斯 dBm)— 分貝,參考功率 1 mW。] — 接收訊號強度指示器。 取 0 到 -100 之間的值,距離 0 越遠,從 Wi-Fi 熱點到設備的過程中訊號功率損失越多。 可以在以下位置找到更多詳細資訊: 維基百科。 這裡我就告訴大家要使用Android類 Wifi管理器 您可以在您選擇的步驟中按照從優秀到糟糕的等級來校準訊號電平:

        val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
        val numberOfLevels = 5
        val level = WifiManager.calculateSignalLevel(level, numberOfLevels)

頻率 — Wi-Fi 熱點的工作頻率 [Hz]。 除了頻率本身之外,您可能對所謂的頻道感興趣。 每個點都有自己的操作純度。 截至撰寫本文時,最受歡迎的 Wi-Fi 熱點範圍是 2.4 GHz。 但是,更準確地說,該點以接近指定頻率的編號頻率將訊息傳輸到您的手機。 頻道數及對應頻率 標準化。 這樣做是為了使附近的點以不同的頻率運行,從而不會相互幹擾,也不會相互降低傳輸速度和品質。 在這種情況下,這些點不是在一個頻率下運行,而是在一個頻率範圍內運行(參數 頻道寬度),稱為通道寬度。 也就是說,在相鄰(不僅是相鄰,甚至是自身的 3 個)頻道上運行的點會相互幹擾。 您可能會發現這個簡單的程式碼很有用,它允許您根據頻率為 2.4 和 5 Ghz 的點的頻率值計算通道號碼:


    /* по частоте определяем номер канала */
    val channel: Int
        get() {
            return if (frequency in 2412..2484) {
                (frequency - 2412) / 5 + 1
            } else if (frequency in 5170..5825) {
                (frequency - 5170) / 5 + 34
            } else {
                -1
            }
        }

能力 - 最有趣的分析領域,需要花費大量時間。 這裡,點的「能力」寫在行中。 在這種情況下,您不必在文件中查找字串解釋的詳細資訊。 以下是該行中可能包含的一些範例:

[WPA-PSK-TKIP+CCMP][WPA2-PSK-TKIP+CCMP][WPS][ESS]
[WPA2-PSK-CCMP][ESS]
[WPA2-PSK-CCMP+TKIP][ESS]
[WPA-PSK-CCMP+TKIP][WPA2-PSK-CCMP+TKIP][ESS]
[ESS][WPS]

5. 理解縮寫與解析能力

值得一提的是,android.net.wifi.* 套件的類別由 Linux 實用程式在幕後使用 wpa_supplicant工作 掃描時capability欄位的輸出結果是flags欄位的副本。

我們將始終如一地行動。 我們首先考慮一種格式的輸出,其中括號內的元素由“-”號分隔:

[WPA-PSK-TKIP+CCMP]
[WPA2-PSK-CCMP]

第一個意義描述了所謂的。 身份驗證方法。 也就是說,裝置和存取點必須執行什麼順序的操作才能使存取點允許其自身被使用以及如何加密有效負載。 在撰寫本文時,最常見的選項是 WPA 和 WPA2,其中每個連接的裝置直接或透過所謂的。 RADIUS 伺服器 (WPA-Enterprice) 透過加密通道提供密碼。 您家中的接入點很可能會根據此方案提供連接。 第二個版本與第一個版本的區別在於它具有更強的密碼:AES 與不安全的 TKIP。 更複雜、更先進的WPA3也正在逐步推出。 理論上,企業解決方案CCKM(思科集中金鑰管理)可能有一個選項,但我從未遇到過。

存取點可能已配置為透過 MAC 位址進行身份驗證。 或者,如果存取點使用過時的WEP演算法提供數據,那麼實際上沒有身份驗證(這裡的金鑰是加密金鑰)。 我們將此類選項歸類為「其他」。
還有一種在公共 Wi-Fi 中流行的隱藏強制門戶檢測的方法 - 透過瀏覽器發出身份驗證請求。 這類接入點在掃描器看來是開放的(從實體連接的角度來看)。 因此,我們將它們歸類為 OPEN。

第二個值可以表示為 密鑰管理演算法。 這是上述認證方法的一個參數。 討論加密金鑰的具體交換方式。 讓我們考慮一下可能的選擇。 EAP - 在提到的 WPA-Enterprice 中使用,使用資料庫來驗證輸入的驗證資料。 SAE-採用先進的WPA3,更耐暴力破解。 PSK - 最常見的選項,涉及輸入密碼並以加密形式傳輸。 IEEE8021X - 根據國際標準(與 WPA 系列支援的標準不同)。 OWE(機會無線加密)是 IEEE 802.11 標準的擴展,針對我們歸類為開放的點。 OWE 透過加密來確保透過不安全網路傳輸的資料的安全性。 當沒有存取鍵時,也可以選擇一個選項,我們將此選項稱為「NONE」。

第三個參數就是所謂的。 加密方案 — 密碼究竟如何用來保護傳輸的資料。 讓我們列出選項。 WEP - 使用 RC4 流密碼,秘密金鑰是加密金鑰,這在現代密碼學領域被認為是不可接受的。 TKIP - 用於 WPA,CKIP - 用於 WPA2。 TKIP+CKIP - 可以在支援 WPA 和 WPA2 的點中指定,以實現向後相容性。

您可以找到一個單獨的 WEP 標記,而不是三個元素:

[WEP]

正如我們上面所討論的,這足以不指定使用金鑰的演算法(該演算法不存在)和加密方法(預設情況下相同)。

現在考慮這個括號:

[ESS]

Wi-Fi工作模式Wi-Fi 網路拓撲。 您可能會遇到 BSS(基本服務集)模式 - 當連接的裝置透過一個存取點進行通訊時。 可以在本地網路上找到。 通常,需要存取點來連接來自不同本地網路的設備,因此它們是擴展服務集 - ESS 的一部分。 IBSS(獨立基本服務集)類型指示設備是對等網路的一部分。

您也可能會看到 WPS 標誌:

[WPS]

WPS(Wi-Fi 保護設定)是一種用於半自動初始化 Wi-Fi 網路的協定。 若要初始化,使用者可以輸入 8 個字元的密碼或按下路由器上的按鈕。 如果您的存取點是第一種類型,而此核取方塊出現在您的存取點名稱旁邊,強烈建議您前往管理面板並停用 WPS 存取。 事實是,通常可以透過 MAC 位址找到 8 位元 PIN,或者可以在可預見的時間內整理出來,從而被不誠實的人利用。

6. 建立模型和解析函數

根據上面的發現,我們將使用資料類別來描述發生的情況:

/* схема аутентификации */
enum class AuthMethod {
    WPA3,
    WPA2,
    WPA, // Wi-Fi Protected Access
    OTHER, // включает в себя Shared Key Authentication и др. использующие mac-address-based и WEP
    CCKM, // Cisco
    OPEN // Open Authentication. Может быть со скрытым Captive Portal Detection - запрос аутентификации через браузер
}

/* алгоритм ввода ключей */
enum class KeyManagementAlgorithm {
    IEEE8021X, // по стандарту
    EAP, // Extensible Authentication Protocol, расширяемый протокол аутентификации
    PSK, // Pre-Shared Key — каждый узел вводит пароль для доступа к сети
    WEP, // в WEP пароль является ключом шифрования (No auth key)
    SAE, // Simultaneous Authentication of Equals - может быть в WPA3
    OWE, // Opportunistic Wireless Encryption - в роутерах новых поколений, публичных сетях типа OPEN
    NONE // может быть без шифрования в OPEN, OTHER
}

/* метод шифрования */
enum class CipherMethod {
    WEP, // Wired Equivalent Privacy, Аналог шифрования трафика в проводных сетях
    TKIP, // Temporal Key Integrity Protocol
    CCMP, // Counter Mode with Cipher Block Chaining Message Authentication Code Protocol,
    // протокол блочного шифрования с кодом аутентичности сообщения и режимом сцепления блоков и счетчика
    // на основе AES
    NONE // может быть без шифрования в OPEN, OTHER
}

/* набор методов шифрования и протоколов, по которым может работать точка */
data class Capability(
    var authScheme: AuthMethod? = null,
    var keyManagementAlgorithm: KeyManagementAlgorithm? = null,
    var cipherMethod: CipherMethod? = null
)

/* Режим работы WiFi (или топология сетей WiFi) */
enum class TopologyMode {
    IBSS, // Эпизодическая сеть (Ad-Hoc или IBSS – Independent Basic Service Set).
    BSS, // Основная зона обслуживания Basic Service Set (BSS) или Infrastructure Mode.
    ESS // Расширенная зона обслуживания ESS – Extended Service Set.
}

現在讓我們來寫一個解析功能欄位的函數:


private fun parseCapabilities(capabilitiesString: String): List < Capability > {
    val capabilities: List < Capability > = capabilitiesString
        .splitByBrackets()
        .filter {
            !it.isTopology() && !it.isWps()
        }
        .flatMap {
            parseCapability(it)
        }
    return
        if (!capabilities.isEmpty()) {
            capabilities
        } else {
            listOf(Capability(AuthMethod.OPEN, KeyManagementAlgorithm.NONE, CipherMethod.NONE))
        }
}

private fun parseCapability(part: String): List < Capability > {
    if (part.contains("WEP")) {
        return listOf(Capability(
            AuthMethod.OTHER,
            KeyManagementAlgorithm.WEP,
            CipherMethod.WEP
        ))
    }

    val authScheme = when {
        part.contains("WPA3") - > AuthMethod.WPA3
        part.contains("WPA2") - > AuthMethod.WPA2
        part.contains("WPA") - > AuthMethod.WPA
        else - > null
    }

    val keyManagementAlgorithm = when {
        part.contains("OWE") - > KeyManagementAlgorithm.OWE
        part.contains("SAE") - > KeyManagementAlgorithm.SAE
        part.contains("IEEE802.1X") - > KeyManagementAlgorithm.IEEE8021X
        part.contains("EAP") - > KeyManagementAlgorithm.EAP
        part.contains("PSK") - > KeyManagementAlgorithm.PSK
        else - > null
    }

    val capabilities = ArrayList < Capability > ()
    if (part.contains("TKIP") || part.contains("CCMP")) {
        if (part.contains("TKIP")) {
            capabilities.add(Capability(
                authScheme ? : AuthMethod.OPEN,
                keyManagementAlgorithm ? : KeyManagementAlgorithm.NONE,
                CipherMethod.TKIP
            ))
        }
        if (part.contains("CCMP")) {
            capabilities.add(Capability(
                authScheme ? : AuthMethod.OPEN,
                keyManagementAlgorithm ? : KeyManagementAlgorithm.NONE,
                CipherMethod.CCMP
            ))
        }
    } else if (authScheme != null || keyManagementAlgorithm != null) {
        capabilities.add(Capability(
            authScheme ? : AuthMethod.OPEN,
            keyManagementAlgorithm ? : KeyManagementAlgorithm.NONE,
            CipherMethod.NONE
        ))
    }

    return capabilities
}

private fun parseTopologyMode(capabilitiesString: String): TopologyMode ? {
    return capabilitiesString
        .splitByBrackets()
        .mapNotNull {
            when {
                it.contains("ESS") - > TopologyMode.ESS
                it.contains("BSS") - > TopologyMode.BSS
                it.contains("IBSS") - > TopologyMode.IBSS
                else - > null
            }
        }
        .firstOrNull()
}

private fun parseWPSAvailable(capabilitiesString: String): Boolean {
    return capabilitiesString
        .splitByBrackets()
        .any {
            it.isWps()
        }
}

private fun String.splitByBrackets(): List < String > {
    val m = Pattern.compile("[(.*?)]").matcher(this)
    val parts = ArrayList < String > ()
    while (m.find()) {
        parts.add(m.group().replace("[", "").replace("]", ""))
    }
    return parts
}

private fun String.isTopology(): Boolean {
    return TopologyMode.values().any {
        this == it.name
    }
}

private fun String.isWps(): Boolean {
    return this == "WPS"
}

8.查看結果

我將掃描網路並向您展示我發現的內容。 顯示的是透過 Log.d 簡單輸出的結果:

Capability of Home-Home [WPA2-PSK-CCMP][ESS][WPS]
...
capabilities=[Capability(authScheme=WPA2, keyManagementAlgorithm=PSK, cipherMethod=CCMP)], topologyMode=ESS, availableWps=true

從應用程式程式碼連接到網路的問題仍未被檢查。 我只想說,為了從行動裝置的作業系統讀取已儲存的密碼,您需要 root 權限並且願意翻閱檔案系統以讀取 wpa_supplicant.conf。 如果應用邏輯需要從外部輸入密碼,可以透過類別連接 android.net.wifi.WifiManager.

謝謝 葉戈爾·波諾馬列夫 以獲得有價值的補充。

如果您認為需要添加或更正某些內容,請在評論中寫下:)

來源: www.habr.com

添加評論