Um dia precisei digitalizar de Android Aplicativos de rede Wi-Fi e obtenção de dados detalhados sobre pontos de acesso.
Aqui tivemos que enfrentar diversas dificuldades: muitas das classes descritas tornaram-se obsoletas (nível de API > 26), o que não se refletiu nela; a descrição de algumas coisas na documentação é mínima (por exemplo, o campo de capacidades da classe no momento em que este artigo foi escrito, quase nada foi descrito, embora contenha muitos dados importantes). A terceira dificuldade pode estar no fato de que ao se aproximar do Wi-Fi pela primeira vez, além de ler a teoria e configurar o roteador via localhost, você terá que lidar com uma série de abreviações que parecem compreensíveis individualmente. Mas pode não ser óbvio como relacioná-los e estruturá-los (o julgamento é subjetivo e depende da experiência anterior).
Este artigo discute como Android Código para obter dados abrangentes sobre o ambiente Wi-Fi sem NDK, sem gambiarras, mas apenas com a ajuda de Android APIs e entender como interpretá-las.
Não vamos atrasar e começar a escrever código.
1. Crie um projeto
Esta nota destina-se a quem já criou mais de uma vez. Android Como se trata de um projeto, vamos pular os detalhes desta seção. O código abaixo será apresentado em Kotlin, com minSdkVersion=23.
2. Permissões de acesso
Para trabalhar com Wi-Fi a partir do aplicativo, você precisará obter diversas permissões do usuário. Conforme , para fazer a varredura da rede em dispositivos com versões de SO posteriores a 8.0, além de acesso para visualizar o estado do ambiente de rede, você precisa de acesso para alterar o estado do módulo Wi-Fi do dispositivo ou acesso às coordenadas (aproximado ou exato). A partir da versão 9.0, você deve solicitar ambos ao usuário e também solicitar explicitamente que ele ative os serviços de localização. Não se esqueça de explicar galantemente ao usuário que isso é um capricho do Google, e não nosso desejo de espioná-lo :)
No total, em AndroidAdicionar Manifest.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"/>
E no código que contém um link para a Activity atual:
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. Crie um BroadcastReceiver e inscreva-se em eventos de atualização de dados sobre a verificação do ambiente de rede 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
}
O método WiFiManager.startScan na documentação está marcado como obsoleto desde a versão 28 da API, mas desativado. sugere usá-lo.
No total, recebemos uma lista de objetos .
4. Veja o ScanResult e entenda os termos
Vejamos alguns campos desta classe e descrevemos o que eles significam:
SSID — Service Set Identifier é o nome da rede
BSSID – Basic Service Set Identifier – Endereço MAC do adaptador de rede (ponto Wi-Fi)
nível — Indicador de intensidade do sinal recebido [dBm (dBm russo) — Decibel, potência de referência 1 mW.] — Um indicador da intensidade do sinal recebido. Assume um valor de 0 a -100, quanto mais longe de 0, mais potência do sinal foi perdida no caminho do ponto Wi-Fi até o seu dispositivo. Mais detalhes podem ser encontrados, por exemplo, em Aqui vou te dizer o que fazer com a ajuda de Android classe você pode calibrar o nível do sinal em uma escala de excelente a péssimo na etapa escolhida:
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val numberOfLevels = 5
val level = WifiManager.calculateSignalLevel(level, numberOfLevels)
freqüência — frequência operacional do ponto Wi-Fi [Hz]. Além da frequência em si, você pode se interessar pelo chamado canal. Cada ponto tem sua própria pureza operacional. No momento em que este artigo foi escrito, a faixa mais popular de pontos Wi-Fi era de 2.4 GHz. Mas, para ser mais preciso, o ponto transmite informações para o seu telefone em uma frequência numerada próxima à indicada. Número de canais e frequências correspondentes . Isso é feito para que pontos próximos operem em frequências diferentes, não interferindo entre si e não reduzindo mutuamente a velocidade e a qualidade da transmissão. Neste caso, os pontos operam não em uma frequência, mas em uma faixa de frequências (parâmetro Largura de banda), chamada de largura do canal. Ou seja, pontos operando em canais adjacentes (e não apenas adjacentes, mas até 3 deles) interferem entre si. Você pode achar útil este código simples, que permite calcular o número do canal a partir do valor da frequência para pontos com frequência de 2.4 e 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
}
}
capacidades - o campo de análise mais interessante, cujo trabalho exigiu muito tempo. Aqui as “capacidades” do ponto estão escritas na linha. Nesse caso, você não precisa procurar detalhes de interpretação de strings na documentação. Aqui estão alguns exemplos do que pode estar nesta linha:
[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. Compreender abreviações e capacidades de análise
Vale ressaltar que as classes do pacote android.net.wifi.* são utilizadas internamente por um utilitário Linux e o resultado de saída no campo de recursos é uma cópia do campo de sinalizadores durante a digitalização.
Agiremos de forma consistente. Vamos primeiro considerar a saída de um formato no qual os elementos entre parênteses são separados por um sinal “-“:
[WPA-PSK-TKIP+CCMP]
[WPA2-PSK-CCMP]
O primeiro significado descreve o chamado. Método de autenticação. Ou seja, qual sequência de ações o dispositivo e o ponto de acesso devem realizar para que o ponto de acesso permita seu uso e como criptografar a carga útil. No momento em que escrevo este post, as opções mais comuns são WPA e WPA2, em que cada dispositivo está conectado diretamente ou através do chamado. O servidor RADIUS (WPA-Enterprice) fornece a senha por meio de um canal criptografado. Muito provavelmente, o ponto de acesso da sua casa fornece uma conexão de acordo com este esquema. A diferença entre a segunda versão e a primeira é que ela possui uma cifra mais forte: AES versus o inseguro TKIP. O WPA3, que é mais complexo e avançado, também está sendo introduzido gradualmente. Teoricamente, pode haver uma opção com a solução empresarial CCKM (Cisco Centralized Key Management), mas nunca a encontrei.
O ponto de acesso pode ter sido configurado para autenticar por endereço MAC. Ou, se o ponto de acesso fornecer dados usando o algoritmo WEP desatualizado, então não há autenticação (a chave secreta aqui é a chave de criptografia). Classificamos essas opções como OUTRAS.
Há também um método popular em wi-fi público com detecção de portal cativo oculto - uma solicitação de autenticação por meio de um navegador. Esses pontos de acesso aparecem para o scanner como abertos (o que são do ponto de vista da conexão física). Portanto, os classificamos como ABERTOS.
O segundo valor pode ser denotado como algoritmo de gerenciamento de chaves. Este é um parâmetro do método de autenticação descrito acima. Fala exatamente como as chaves de criptografia são trocadas. Vamos considerar as opções possíveis. EAP - utilizado no mencionado WPA-Enterprice, utiliza um banco de dados para verificar os dados de autenticação inseridos. SAE – utilizado em WPA3 avançado, mais resistente à força bruta. PSK - a opção mais comum, envolve inserir uma senha e transmiti-la de forma criptografada. IEEE8021X - conforme padrão internacional (diferente daquele suportado pela família WPA). OWE (Opportunistic Wireless Encryption) é uma extensão do padrão IEEE 802.11 para pontos que classificamos como ABERTOS. OWE garante a segurança dos dados transmitidos por uma rede insegura, criptografando-os. Uma opção também é possível quando não há chaves de acesso, vamos chamar essa opção de NONE.
O terceiro parâmetro é o chamado. esquemas de criptografia — como exatamente a cifra é usada para proteger os dados transmitidos. Vamos listar as opções. WEP - usa uma cifra de fluxo RC4, a chave secreta é a chave de criptografia, considerada inaceitável no mundo da criptografia moderna. TKIP - usado em WPA, CKIP - em WPA2. TKIP+CKIP - pode ser especificado em pontos compatíveis com WPA e WPA2 para compatibilidade com versões anteriores.
Em vez de três elementos, você pode encontrar uma marca WEP solitária:
[WEP]
Como discutimos acima, isso é suficiente não para especificar o algoritmo de uso de chaves, que não existe, e o método de criptografia, que é o mesmo por padrão.
Agora considere este colchete:
[ESS]
Ele Modo de operação Wi-Fi ou Topologia de rede Wi-Fi. Você pode encontrar o modo BSS (Basic Service Set) - quando há um ponto de acesso através do qual os dispositivos conectados se comunicam. Pode ser encontrado em redes locais. Via de regra, os pontos de acesso são necessários para conectar dispositivos de diferentes redes locais, portanto fazem parte de Extended Service Sets - ESS. O tipo IBSSs (Independent Basic Service Sets) indica que o dispositivo faz parte de uma rede ponto a ponto.
Você também pode ver a sinalização WPS:
[WPS]
WPS (Wi-Fi Protected Setup) é um protocolo para inicialização semiautomática de uma rede Wi-Fi. Para inicializar, o usuário digita uma senha de 8 caracteres ou pressiona um botão no roteador. Se o seu ponto de acesso for do primeiro tipo e esta caixa de seleção aparecer ao lado do nome do seu ponto de acesso, é altamente recomendável ir ao painel de administração e desabilitar o acesso WPS. O fato é que muitas vezes o PIN de 8 dígitos pode ser descoberto pelo endereço MAC, ou pode ser resolvido em um tempo previsível, do qual alguém pode tirar vantagem desonestamente.
6. Crie um modelo e função de análise
Com base no que descobrimos acima, descreveremos o que aconteceu usando classes de dados:
/* схема аутентификации */
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.
}
Agora vamos escrever uma função que irá analisar o campo de capacidades:
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. Veja o resultado
Vou escanear a rede e mostrar o que encontrei. São mostrados os resultados da saída simples via Log.d:
Capability of Home-Home [WPA2-PSK-CCMP][ESS][WPS]
...
capabilities=[Capability(authScheme=WPA2, keyManagementAlgorithm=PSK, cipherMethod=CCMP)], topologyMode=ESS, availableWps=true
A questão da conexão à rede a partir do código do aplicativo permaneceu sem ser examinada. Direi apenas que para ler senhas salvas no sistema operacional de um dispositivo móvel, você precisa de direitos de root e vontade de vasculhar o sistema de arquivos para ler wpa_supplicant.conf. Se a lógica da aplicação exigir a inserção de uma senha externa, a conexão poderá ser feita através da classe .
Obrigado para adições valiosas.
Se você acha que algo precisa ser acrescentado ou corrigido, escreva nos comentários :)
Fonte: habr.com
