Wi-Fi і шмат іншых абрэвіятур. Як у Android дадатку атрымаць дадзеныя аб вузлах Wi-Fi і не апухнуць

Аднойчы мне спатрэбілася сканаваць з Android прыкладання сеткі Wi-Fi і атрымліваць падрабязную выкладку дадзеных аб кропках доступу.

Тут прыйшлося сутыкнуцца з некалькімі цяжкасцямі: у оф.дакументацыі Android многія апісаныя класы сталі deprecated (API level> 26), што ніяк не было ў ёй адлюстравана; апісанне некаторых рэчаў у дакументацыі мінімальна (напрыклад поле capabilities класа ScanResult на момант напісання не апісана амаль ніяк, хаця змяшчае шмат важных дадзеных). Трэцяя складанасць можа складацца ў тым, што пры першай блізкасці з Wi-Fi, выдатнай ад чытання тэорыі і налады роўтара па localhost, прыходзіцца мець справу з шэрагам абрэвіятур, якія здаюцца зразумелымі па асобнасці. Але можа быць не відавочна, як іх суаднесці і структураваць (меркаванне суб'ектыўна і залежыць ад папярэдняга досведу).

У дадзеным артыкуле разгледжана як з Android кода атрымаць вычарпальныя дадзеныя аб Wi-Fi асяроддзі без NDK, хакаў, а толькі з дапамогай 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"/>

А ў кодзе, у якім ёсць спасылка на бягучую Activity:

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 у дакументацыі пазначаны як depricated з версіі API 28, але оф. кіраўніцтва прапануе выкарыстоўваць яго.

Разам атрымалі спіс аб'ектаў ScanResult.

4. Глядзім на ScanResult і разбіраемся ў тэрмінах

Паглядзім на некаторыя палі гэтага класа і апішам, што яны азначаюць:

ідэнтыфікатар SSID — Service Set Identifier - гэта назва сеткі

BSSID – Basic Service Set Identifier – MAC адрас сеткавага адаптара (Wi-Fi кропкі)

ўзровень - Received Signal Strength Indicator [dBm (рускае дБм) - Дэцыбел, апорная магутнасць 1 мВт.] - Паказчык ўзроўню прыманага сігналу. Прымае значэнне ад 0 да -100, чым далей ад 0, тым больш магутнасці сігналу згубілася па шляху ад Wi-Fi кропкі да вашай прылады. Падрабязней можна паглядзець напрыклад на Вікіпедыі. Тут жа раскажу, што з дапамогай Android класа WifiManager можна праградуіраваць узровень сігналу па шкале ад выдатнага да жудаснага з абраным вамі крокам:

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

частата - частата працы кропкі Wi-Fi [Гц]. Апроч самай частаты вас можа зацікавіць так званы канал. У кожнай кропкі ёсць свая працоўная чысціня. На момант напісання тэксту найболей папулярным дыяпазонам Wi-Fi кропак з'яўляецца 2.4 GHz. Але, калі быць дакладней, кропка перадае інфармацыю на ваш тэлефон на пранумараванай частаце, блізкай да названай. Колькасць каналаў і значэння адпаведных частот стандартызаваны. Гэта зроблена для таго, каб кропкі паблізу працавалі на розных частотах, тым самым не ствараючы перашкоды адзін аднаму і ўзаемна не паніжаючы хуткасць і якасць перадачы. Пры гэтым кропкі працуюць не на адной частаце, а на дыяпазоне частот (парыметр channelWidth), званым шырынёй канала. Гэта значыць кропкі, якія працуюць на суседніх (і не толькі на суседніх, а нават на 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. Разбіраемся ў абрэвіятурах і парсім capabilities

Варта згадаць, што класы пакета android.net.wifi.* выкарыстоўвае пад капотам linux-утыліту wpa_прасільнік і вынік вываду ў поле capabilities з'яўляецца копіяй поля flags пры сканаванні.

Будзем дзейнічаць паслядоўна. Разгледзім спачатку выснова такога фармату, пры якім усярэдзіне дужак элементы аддзеленыя знакам «-«:

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

Першае значэнне апісвае т.зв. метад аўтэнтыфікацыі (authentication). Гэта значыць, якую паслядоўнасць дзеянняў павінны вырабіць прыладу і кропка доступу, каб кропка доступу дазволіла сабой карыстацца і якім чынам шыфраваць карысную нагрузку. На момант напісання паста самыя частыя варыянты гэта WPA і WPA2, пры якім альбо кожная якая падключаецца прылада напроста, альбо праз т.зв. RADIUS-сервер (WPA-Enterprice) падае пароль па зашыфраваным канале. Хутчэй за ўсё ў вас дома кропка доступу дае падключэнне па гэтай схеме. Адрозненне другой версіі ад першай у больш устойлівым шыфры: AES супраць небяспечнага TKIP. Таксама паступова ўкараняецца WPA3, больш складаны і прасунуты. Тэарытычна можа сустрэцца варыянт з enterprice-рашэннем CCKM (Cisco Centralized Key Managment), але мне так і не сустрэўся.

Кропка доступу магла быць настроена на аўтэнтыфікацыю па MAC-адрасу. Ці, калі кропка доступу падае дадзеныя па састарэлым алгарытме WEP, то аўтэнтыфікацыі фактычна няма (сакрэтны ключ тут і з'яўляецца ключом шыфравання). Такія варыянты аднясем да тыпу OTHER.
Яшчэ ёсць упадабаны ў грамадскіх wi-fi метад са ўтоеным Captive Portal Detection - запыт аўтэнтыфікацыі праз браўзэр. Такія кропкі доступу выглядаюць для сканара як адчыненыя (якімі з кропкі зроку фізічнага падлучэння і з'яўляюцца). Таму аднясем іх да тыпу OPEN.

Другое значэнне можна пазначыць як алгарытм выкарыстання ключоў (key management). З'яўляецца параметрам метаду аўтэнтыфікацыі, аб якім напісана вышэй. Гаворыць аб тым, як менавіта адбываецца абмен ключамі шыфравання. Разгледзім магчымыя варыянты. EAP - выкарыстоўваецца ў згаданым WPA-Enterprice, выкарыстоўвае базу дадзеных для зверкі уведзеных аўтэнтыфікацыйных дадзеных. SAE - выкарыстоўваецца ў прасунутым WPA3, больш устойлівая да перабору. PSK - самы часты варыянт, мае на ўвазе ўвод пароля і яго перадачу ў зашыфраваным выглядзе. IEEE8021X – па міжнародным стандарце (выдатнаму ад падтрыманым сямействам WPA). OWE (Opportunistic Wireless Encryption) з'яўляецца пашырэннем стандарту IEEE 802.11, для кропак, якія мы аднеслі да тыпу OPEN. OWE забяспечвае бяспеку дадзеных, якія перадаюцца па неабароненай сетцы, за кошт іх шыфравання. Таксама магчымы варыянт калі ключоў доступу няма, назавем такі варыянт NONE.

Трэцім параметрам з'яўляецца т.зв. метад шыфравання (encryption schemes) - як менавіта выкарыстоўваецца шыфр для зашытых перадаваных дадзеных. Пералічоны варыянты. WEP – выкарыстоўвае струменевы шыфр RC4, сакрэтны ключ з'яўляецца ключом шыфравання, што ў свеце сучаснай крыптаграфіі лічыцца непрымальным. TKIP - выкарыстоўваецца ў WPA, CKIP - у WPA2. TKIP+CKIP - можа быць паказаны ў кропках якія ўмеюць WPA і WPA2 для зваротнай сумяшчальнасці.

Замест трох элементаў можна сустрэць адзінокую пазнаку WEP:

[WEP]

Як мы абмеркавалі вышэй, гэтага дастаткова каб не канкрэтызаваць алгарытм выкарыстання ключоў, якога няма, і метаду шыфравання, якое адно па-змаўчанні.

Цяпер разгледзім такі клямарчык:

[ESS]

Гэта рэжым працы Wi-Fi або тапалогія сетак Wi-Fi. Вам можа сустрэцца Рэжым BSS (Basic Service Set) - калі ёсць адна кропка доступу, праз якую маюць зносіны падлучаныя прылады. Можна сустрэць у лакальных сетках. Як правіла кропкі доступу патрэбныя для таго, каб злучаць прылады з розных лакальных сетак, таму яны з'яўляюцца часткай Extended Service Sets – ESS. Тып IBSSs (Independent Basic Service Sets) кажа аб тым, што прылада з'яўляецца часткай Peer-to-Peer сеткі.

Яшчэ можа трапіцца сцяг WPS:

[WPS]

WPS (Wi-Fi Protected Setup) – пратакол паўаўтаматычнай ініцыялізацыі сеткі Wi-Fi. Для ініцыялізацыі карыстач альбо ўводзіць 8-знакавы пароль, альбо заціскае кнопку на роўтары. Калі ваш пункт доступу адносіцца да першага тыпу і гэты сцяжок высветліўся насупраць імя вашага пункту доступу, вам настойліва рэкамендуецца зайсці ў адмінку і адключыць доступ па WPS. Справа ў тым, што часта 8-знакавы PIN можна пазнаць па MAC-адрасу, альбо перабраць за аглядны час, чым хтосьці нячысты на руку зможа скарыстацца.

6. Ствараем мадэль і функцыю парсінгу

На аснове таго, што высветлілі вышэй апішам data-класамі тое, што атрымалася:

/* схема аутентификации */
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.
}

Цяпер напішам функцыю, якая будзе парсіць поле capabilities:


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.

Дзякуй Ягору Панамараву за каштоўныя дапаўненні.

Калі лічыце, што трэба нешта дадаць ці выправіць, пішыце ў каментары 🙂

Крыніца: habr.com

Дадаць каментар