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 开始,您必须提示用户两者,并明确请求用户打开位置服务。 不要忘记勇敢地向用户解释这是谷歌的突发奇想,而不是我们想要监视他:)

因此,在 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.

谢谢 叶戈尔·波诺马列夫 以获得有价值的补充。

如果您认为需要添加或更正某些内容,请在评论中写下:)

来源: habr.com

添加评论