Wi-Fi kaj multaj aliaj mallongigoj. Kiel akiri datumojn pri Wifi-nodoj en Android-apliko sen ŝveliĝi

Iun tagon mi bezonis skani Wifi-retojn de Android-aplikoj kaj akiri detalajn datumojn pri alirpunktoj.

Ĉi tie ni devis alfronti plurajn malfacilaĵojn: off.Android-dokumentado multaj el la priskribitaj klasoj iĝis malrekomenditaj (API-nivelo > 26), kio ne estis reflektita en ĝi; la priskribo de kelkaj aferoj en la dokumentaro estas minimuma (ekzemple, la kapablokampo de la klaso ScanResult dum la skribado, preskaŭ nenio estas priskribita, kvankam ĝi enhavas multajn gravajn datumojn). La tria malfacilaĵo povas kuŝi en la fakto, ke kiam vi unue alproksimiĝas al Wi-Fi, krom legi la teorion kaj agordi la enkursigilon per localhost, vi devas trakti kelkajn mallongigojn, kiuj ŝajnas kompreneblaj individue. Sed eble ne estas evidente kiel rilati kaj strukturi ilin (juĝo estas subjektiva kaj dependas de antaŭa sperto).

Ĉi tiu artikolo diskutas kiel akiri ampleksajn datumojn pri la Wi-Fi-medio de Android-kodo sen NDK, hakoj, sed nur uzante la Android-API kaj kompreni kiel interpreti ĝin.

Ni ne prokrastu kaj komencu skribi kodon.

1. Kreu projekton

Ĉi tiu noto estas destinita por tiuj, kiuj kreis Android-projekton pli ol unufoje, do ni preterlasas la detalojn de ĉi tiu ero. La suba kodo estos prezentita en Kotlin, minSdkVersion=23.

2. Alirpermesoj

Por labori kun Wi-Fi de la aplikaĵo, vi devos akiri plurajn permesojn de la uzanto. Konforme dokumentado, por skani la reton en aparatoj kun OS-versioj post 8.0, krom aliro por vidi la staton de la retomedio, vi bezonas aŭ aliron por ŝanĝi la staton de la Wi-Fi-modulo de la aparato, aŭ aliron al koordinatoj (proksimuma aŭ preciza). Ekde la versio 9.0, vi devas peti la uzanton por ambaŭ, kaj ankaŭ eksplicite peti la uzanton ŝalti lokservojn. Ne forgesu galante klarigi al la uzanto, ke tio estas la kaprico de Guglo, kaj ne nia deziro spioni lin :)

Do, en AndroidManifest.xml ni aldonos:

    <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"/>

Kaj en la kodo kiu enhavas ligilon al la nuna Agado:

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. Kreu BroadcastReceiver kaj abonu datumajn ĝisdatigajn eventojn pri skanado de la reto-medio de WiFi

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
}

La WiFiManager.startScan-metodo en la dokumentaro estas markita kiel senvalorigita ekde API-versio 28, sed malŝaltita. gvidi sugestas uzi ĝin.

Entute ni ricevis liston de objektoj ScanResult.

4. Rigardu ScanResult kaj komprenu la terminojn

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

SSID — Serva Aro-Identigilo estas la nomo de la reto

BSSID - Baza Serva Aro-Identigilo - MAC-adreso de la retadaptilo (Wi-Fi-punkto)

nivelo — Indikilo de la forto de ricevita signalo [dBm (ruse dBm) — decibelo, referenca potenco 1 mW.] — Indikilo de la ricevita signalforto. Prenas valoron de 0 ĝis -100, ju pli for de 0, des pli da signalpotenco perdiĝis survoje de la Wifi-punkto al via aparato. Pliaj detaloj troveblas, ekzemple, ĉe Vikipedio. Ĉi tie mi diros al vi tion uzante la Android-klason WifiManager vi povas kalibri la signalnivelon sur skalo de bonega ĝis terura en la paŝo, kiun vi elektas:

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

frekvenco — operacia frekvenco de la Wifi-punkto [Hz]. Krom la ofteco mem, vi eble interesiĝas pri la tiel nomata kanalo. Ĉiu punkto havas sian propran operacian purecon. Dum la skribado, la plej populara gamo de Wi-Fi-punktoj estas 2.4 GHz. Sed, por esti pli preciza, la punkto transdonas informojn al via telefono je numerita frekvenco proksima al tiu nomita. Nombro de kanaloj kaj respondaj frekvencoj normigita. Ĉi tio estas farita por ke proksimaj punktoj funkciigu je malsamaj frekvencoj, tiel ne malhelpante unu la alian kaj ne reciproke reduktante la rapidecon kaj kvaliton de dissendo. En ĉi tiu kazo, la punktoj funkcias ne je unu frekvenco, sed en gamo da frekvencoj (parametro channelWidth), nomita la kanallarĝo. Tio estas, punktoj funkciantaj sur apudaj (kaj ne nur apudaj, sed eĉ 3 de si mem) kanaloj malhelpas unu la alian. Vi eble trovos ĉi tiun simplan kodon utila, kiu ebligas al vi kalkuli la kanalan nombron el la frekvenca valoro por punktoj kun frekvenco de 2.4 kaj 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
            }
        }

Kapabloj - la plej interesa kampo por analizo, laboro kun kiu postulis multe da tempo. Ĉi tie la "kapabloj" de la punkto estas skribitaj en la linio. En ĉi tiu kazo, vi ne devas serĉi detalojn pri korda interpreto en la dokumentado. Jen kelkaj ekzemploj de kio povus esti en ĉi tiu linio:

[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. Kompreni mallongigojn kaj analizajn kapablojn

Menciindas, ke la klasoj de la pako android.net.wifi.* estas uzataj sub la kapuĉo de Linuksa ilo. wpa_supplicant kaj la rezulta rezulto en la kampo de kapabloj estas kopio de la kampo de flagoj dum skanado.

Ni agos konsekvence. Ni unue konsideru la eligon de formato en kiu elementoj en krampoj estas apartigitaj per signo “-“:

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

La unua signifo priskribas la tn. aŭtentikiga metodo. Tio estas, kian sekvencon de agoj devas la aparato kaj la alirpunkto plenumi por ke la alirpunkto lasu sin uzi kaj kiel ĉifri la utilan ŝarĝon. Al la horo de skribi ĉi tiun afiŝon, la plej oftaj ebloj estas WPA kaj WPA2, en kiuj aŭ ĉiu konektita aparato rekte aŭ tra la tn. La RADIUS-servilo (WPA-Enterprice) provizas la pasvorton per ĉifrita kanalo. Plej verŝajne, la alirpunkto en via hejmo provizas konekton laŭ ĉi tiu skemo. La diferenco inter la dua versio kaj la unua estas, ke ĝi havas pli fortan ĉifron: AES kontraŭ la nesekura TKIP. WPA3, kiu estas pli kompleksa kaj progresinta, ankaŭ estas iom post iom enkondukata. Teorie, povas ekzisti opcio kun la entreprena solvo CCKM (Cisco Centralized Key Management), sed mi neniam renkontis ĝin.

La alirpunkto eble estis agordita por aŭtentikigi per MAC-adreso. Aŭ, se la alirpunkto provizas datumojn uzante la malmodernan WEP-algoritmon, tiam fakte ne ekzistas aŭtentikigo (la sekreta ŝlosilo ĉi tie estas la ĉifrada ŝlosilo). Ni klasifikas tiajn opciojn kiel ALIAJ.
Ekzistas ankaŭ metodo, kiu estas populara en publika wi-fi kun kaŝita Kaptive Portal Detection - aŭtentikigpeto per retumilo. Tiaj alirpunktoj ŝajnas al la skanilo malfermitaj (kiu ili estas el la vidpunkto de la fizika konekto). Tial, ni klasifikas ilin kiel MALFERMA.

La dua valoro povas esti indikita kiel ŝlosila administra algoritmo. Ĉi tio estas parametro de la aŭtentikiga metodo priskribita supre. Parolas pri ĝuste kiel ĉifradaj ŝlosiloj estas interŝanĝitaj. Ni konsideru la eblajn eblojn. EAP - uzata en la menciita WPA-Enterprice, uzas datumbazon por kontroli la enmetitajn aŭtentigajn datumojn. SAE - uzata en progresinta WPA3, pli imuna al krudforto. PSK - la plej ofta opcio, implikas enigi pasvorton kaj transdoni ĝin en ĉifrita formo. IEEE8021X - laŭ internacia normo (malsama de tiu subtenata de la familio WPA). OWE (Opportunistic Wireless Encryption) estas etendaĵo de la IEEE 802.11 normo por punktoj, kiujn ni klasifikis kiel MALFERMA. OWE certigas la sekurecon de datumoj transdonitaj tra nesekurigita reto per ĉifrado de ĝi. Opcio ankaŭ eblas kiam ne ekzistas alirŝlosiloj, ni nomu ĉi tiun opcion NENIUJ.

La tria parametro estas la tn. ĉifradaj skemoj — kiel ĝuste la ĉifro estas uzata por protekti la transdonitajn datumojn. Ni listigu la eblojn. WEP - uzas RC4-fluan ĉifron, la sekreta ŝlosilo estas la ĉifrada ŝlosilo, kiu estas konsiderata neakceptebla en la mondo de moderna kriptografio. TKIP - uzata en WPA, CKIP - en WPA2. TKIP+CKIP - povas esti specifita en punktoj kapablaj je WPA kaj WPA2 por retrokongruo.

Anstataŭ tri elementoj, vi povas trovi solecan WEP-markon:

[WEP]

Kiel ni diskutis supre, ĉi tio sufiĉas por ne specifi la algoritmon por uzi ŝlosilojn, kiu ne ekzistas, kaj la ĉifradan metodon, kiu estas la sama defaŭlte.

Nun konsideru ĉi tiun krampon:

[ESS]

ĉi Funkcia reĝimo Wi-FiTopologio de reto WiFi. Vi povas renkonti BSS (Basic Service Set) reĝimon - kiam ekzistas unu alirpunkto tra kiu konektitaj aparatoj komunikas. Troveblas en lokaj retoj. Kiel regulo, alirpunktoj estas bezonataj por konekti aparatojn de malsamaj lokaj retoj, do ili estas parto de Plilongigitaj Servaj Aroj - ESS. La tipo IBSSs (Independent Basic Service Sets) indikas ke la aparato estas parto de Peer-to-Peer reto.

Vi ankaŭ povas vidi la WPS-flagon:

[WPS]

WPS (Wi-Fi Protected Setup) estas protokolo por duonaŭtomata inicialigo de WiFi-reto. Por pravalorigi, la uzanto aŭ enigas 8-karakteran pasvorton aŭ premas butonon sur la enkursigilo. Se via alirpunkto estas de la unua tipo kaj ĉi tiu markobutono aperas apud la nomo de via alirpunkto, vi forte rekomendas iri al la administra panelo kaj malŝalti WPS-aliron. La fakto estas, ke ofte la 8-cifera PIN estas eltrovebla per la MAC-adreso, aŭ ĝi povas esti ordigita en antaŭvidebla tempo, kiun iu malhoneste povas utiligi.

6. Kreu modelon kaj analizan funkcion

Surbaze de tio, kion ni eksciis supre, ni priskribos kio okazis uzante datumklasojn:

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

Nun ni skribu funkcion, kiu analizos la kampon de kapabloj:


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. Vidu la rezulton

Mi skanos la reton kaj montros al vi tion, kion mi trovis. Montriĝas la rezultoj de simpla eligo per Log.d:

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

La afero pri konekto al la reto de la aplika kodo restis neekzamenita. Mi nur diros, ke por legi konservitajn pasvortojn de la OS de poŝtelefono, vi bezonas radikrajtojn kaj volon traserĉi la dosiersistemon por legi wpa_supplicant.conf. Se la aplika logiko postulas enigi pasvorton de ekstere, la konekto povas esti farita per la klaso android.net.wifi.WifiManager.

Спасибо Egor Ponomarev por valoraj aldonoj.

Se vi pensas, ke io devas esti aldonita aŭ korektita, skribu en la komentoj :)

fonto: www.habr.com

Aldoni komenton