Wi-Fi и много других аббревиатур. Как в Android приложении получить данные об узлах Wi-Fi и не опухнуть

Однажды мне понадобилось сканировать из Android приложения сети Wi-Fi и получать подробную выкладку данных о точках доступа.

در اینجا ما مجبور شدیم با چندین مشکل روبرو شویم: خاموش.اسناد اندروید многие описанные классы стали deprecated (API level > 26), что никак не было в ней отражено; описание некоторых вещей в документации минимально (например поле capabilities класса اسکن نتیجه на момент написания не описано почти никак, хотя содержит много важных данных). Третья сложность может заключаться в том, что при первой близости с Wi-Fi, отличной от чтения теории и настройки роутера по localhost, приходится иметь дело с рядом аббревиатур, которые кажутся понятными по отдельности. Но может быть не очевидно, как их соотнести и структурировать (суждение субъективно и зависит от предыдущего опыта).

در این مقاله نحوه به دست آوردن اطلاعات جامع در مورد محیط Wi-Fi از کد اندروید بدون NDK، هک، اما فقط با استفاده از API Android و درک نحوه تفسیر آن بحث می شود.

معطل نکنیم و شروع به کدنویسی کنیم.

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 در اسناد از نسخه 28 API منسوخ شده اما خاموش علامت گذاری شده است. راهنمایی استفاده از آن را پیشنهاد می کند.

در مجموع، لیستی از اشیاء را دریافت کردیم اسکن نتیجه.

4. Смотрим на ScanResult и разбираемся в терминах

بیایید به برخی از فیلدهای این کلاس نگاهی بیندازیم و معنی آنها را شرح دهیم:

SSID — شناسه مجموعه سرویس نام شبکه است

BSSID – شناسه مجموعه سرویس اولیه – آدرس MAC آداپتور شبکه (نقطه Wi-Fi)

سطح — Received Signal Strength Indicator [dBm (русское дБм) — Децибел, опорная мощность 1 мВт.] — Показатель уровня принимаемого сигнала. Принимает значение от 0 до -100, чем дальше от 0, тем больше мощности сигнала потерялось по пути от Wi-Fi точки к вашему устройству. Подробнее можно посмотреть например на ویکی پدیا. Здесь же расскажу, что с помощью Android класса وای فای منیجر در مرحله ای که انتخاب می کنید می توانید سطح سیگنال را در مقیاسی از عالی تا وحشتناک کالیبره کنید:

        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. Но, если быть точнее, точка передает информацию на ваш телефон на пронумерованной частоте, близкой к названной. Количество каналов и значения соответствующих частот استاندارد شده. این کار به گونه ای انجام می شود که نقاط نزدیک در فرکانس های مختلف کار کنند و در نتیجه با یکدیگر تداخل نداشته باشند و سرعت و کیفیت انتقال را کاهش دهند. در این حالت، نقاط در یک فرکانس کار نمی کنند، بلکه در محدوده ای از فرکانس ها (پارامتر پهنای کانال) عرض کانال نامیده می شود. یعنی نقاطی که روی کانال های مجاور (و نه تنها مجاور، بلکه حتی 3 کانال از خودشان) کار می کنند با یکدیگر تداخل دارند. ممکن است این کد ساده برای شما مفید باشد که به شما امکان می دهد شماره کانال را از مقدار فرکانس برای نقاط با فرکانس 2.4 و 5 گیگاهرتز محاسبه کنید:


    /* по частоте определяем номер канала */
    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_supplicant و نتیجه خروجی در قسمت قابلیت ها یک کپی از فیلد flags هنگام اسکن است.

ما به طور مداوم عمل خواهیم کرد. اجازه دهید ابتدا خروجی قالبی را در نظر بگیریم که در آن عناصر داخل پرانتز با علامت "-" از هم جدا می شوند:

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

معنای اول به اصطلاح توصیف می کند. روش احراز هویت. یعنی دستگاه و اکسس پوینت چه توالی اقداماتی را باید انجام دهند تا اکسس پوینت به خود اجازه استفاده بدهد و چگونه بار را رمزگذاری کند. در زمان نوشتن این پست، رایج ترین گزینه ها WPA و WPA2 هستند که در آن یا هر دستگاه به طور مستقیم و یا از طریق به اصطلاح متصل می شود. سرور RADIUS (WPA-Enterprice) رمز عبور را از طریق یک کانال رمزگذاری شده ارائه می دهد. به احتمال زیاد، نقطه دسترسی در خانه شما اتصالی را مطابق با این طرح فراهم می کند. تفاوت نسخه دوم با نسخه اول این است که رمز قوی تری دارد: AES در مقابل TKIP ناامن. WPA3 که پیچیده تر و پیشرفته تر است نیز به تدریج معرفی می شود. از نظر تئوری، ممکن است گزینه ای با راه حل Enterprice CCKM (Cisco Centralized Key Management) وجود داشته باشد، اما من هرگز با آن برخورد نکرده ام.

نقطه دسترسی ممکن است برای احراز هویت با آدرس MAC پیکربندی شده باشد. یا اگر نقطه دسترسی داده ها را با استفاده از الگوریتم قدیمی WEP ارائه کند، در واقع هیچ احراز هویتی وجود ندارد (کلید مخفی در اینجا کلید رمزگذاری است). ما چنین گزینه هایی را به عنوان OTHER طبقه بندی می کنیم.
همچنین روشی وجود دارد که در وای فای عمومی با تشخیص پورتال مخفی معروف است - درخواست احراز هویت از طریق مرورگر. چنین نقاط دسترسی برای اسکنر باز به نظر می رسند (که از نقطه نظر اتصال فیزیکی هستند). بنابراین، ما آنها را به عنوان OPEN طبقه بندی می کنیم.

مقدار دوم را می توان به عنوان نشان داد الگوریتم مدیریت کلید. Является параметром метода аутентификации, о котором написано выше. Говорит о том, как именно происходит обмен ключами шифрования. Рассмотрим возможные варианты. EAP — используется в упомянутом WPA-Enterprice, использует базу данных для сверки введеных аутентификационных данных. SAE — используется в продвинутом WPA3, более устойчива к перебору. PSK — самый частый вариант, подразумевает ввод пароля и его передачу в зашифрованном виде. IEEE8021X — по международному стандарту (отличному от поддержанным семейством WPA). OWE (Opportunistic Wireless Encryption) является расширением стандарта IEEE 802.11, для точек, которые мы отнесли к типу OPEN. OWE обеспечивает безопасность данных, передаваемых по незащищенной сети, за счет их шифрования. Также возможен варинант когда ключей доступа нет, назовем такой вариант NONE.

پارامتر سوم به اصطلاح است. طرح های رمزگذاری - دقیقاً چگونه رمز برای محافظت از داده های ارسال شده استفاده می شود. بیایید گزینه ها را فهرست کنیم. WEP - از رمز جریانی RC4 استفاده می کند، کلید مخفی کلید رمزگذاری است که در دنیای رمزنگاری مدرن غیرقابل قبول تلقی می شود. TKIP - در WPA، CKIP - در WPA2 استفاده می شود. TKIP + CKIP - می تواند در نقاط دارای قابلیت WPA و WPA2 برای سازگاری با عقب مشخص شود.

به جای سه عنصر، می توانید یک علامت WEP تنها پیدا کنید:

[WEP]

همانطور که در بالا بحث کردیم، این کافی است تا الگوریتم استفاده از کلیدها را که وجود ندارد و روش رمزگذاری که به طور پیش فرض یکسان است، مشخص نکنید.

حال این براکت را در نظر بگیرید:

[ESS]

آن حالت کار Wi-Fi یا توپولوژی شبکه وای فای. هنگامی که یک نقطه دسترسی وجود دارد که دستگاه های متصل از طریق آن با هم ارتباط برقرار می کنند، ممکن است با حالت BSS (تنظیم خدمات پایه) مواجه شوید. را می توان در شبکه های محلی یافت. به عنوان یک قاعده، نقاط دسترسی برای اتصال دستگاه ها از شبکه های محلی مختلف مورد نیاز است، بنابراین آنها بخشی از مجموعه خدمات گسترده - ESS هستند. نوع IBSS (مجموعه های خدمات پایه مستقل) نشان می دهد که دستگاه بخشی از یک شبکه Peer-to-Peer است.

همچنین ممکن است پرچم WPS را ببینید:

[WPS]

WPS (Wi-Fi Protected Setup) پروتکلی برای مقداردهی اولیه نیمه خودکار یک شبکه Wi-Fi است. برای مقداردهی اولیه، کاربر یا یک رمز عبور 8 کاراکتری وارد می کند یا دکمه ای را روی روتر فشار می دهد. اگر نقطه دسترسی شما از نوع اول است و این چک باکس در کنار نام نقطه دسترسی شما ظاهر می شود، اکیداً به شما توصیه می شود که به پنل مدیریت بروید و دسترسی WPS را غیرفعال کنید. واقعیت این است که اغلب می توان پین 8 رقمی را با آدرس MAC پیدا کرد، یا می توان آن را در یک زمان قابل پیش بینی مرتب کرد، که یک فرد غیر صادقانه می تواند از آن استفاده کند.

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

اضافه کردن نظر