/*
|
* Copyright (C) 2015 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
package com.android.server.wifi.util;
|
|
import android.net.wifi.ScanResult;
|
import android.net.wifi.ScanResult.InformationElement;
|
import android.util.Log;
|
|
import com.android.server.wifi.ByteBufferReader;
|
import com.android.server.wifi.hotspot2.NetworkDetail;
|
import com.android.server.wifi.hotspot2.anqp.Constants;
|
|
import java.nio.BufferUnderflowException;
|
import java.nio.ByteBuffer;
|
import java.nio.ByteOrder;
|
import java.util.ArrayList;
|
import java.util.BitSet;
|
|
public class InformationElementUtil {
|
private static final String TAG = "InformationElementUtil";
|
|
public static InformationElement[] parseInformationElements(byte[] bytes) {
|
if (bytes == null) {
|
return new InformationElement[0];
|
}
|
ByteBuffer data = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
|
ArrayList<InformationElement> infoElements = new ArrayList<>();
|
boolean found_ssid = false;
|
while (data.remaining() > 1) {
|
int eid = data.get() & Constants.BYTE_MASK;
|
int elementLength = data.get() & Constants.BYTE_MASK;
|
|
if (elementLength > data.remaining() || (eid == InformationElement.EID_SSID
|
&& found_ssid)) {
|
// APs often pad the data with bytes that happen to match that of the EID_SSID
|
// marker. This is not due to a known issue for APs to incorrectly send the SSID
|
// name multiple times.
|
break;
|
}
|
if (eid == InformationElement.EID_SSID) {
|
found_ssid = true;
|
}
|
|
InformationElement ie = new InformationElement();
|
ie.id = eid;
|
ie.bytes = new byte[elementLength];
|
data.get(ie.bytes);
|
infoElements.add(ie);
|
}
|
return infoElements.toArray(new InformationElement[infoElements.size()]);
|
}
|
|
/**
|
* Parse and retrieve the Roaming Consortium Information Element from the list of IEs.
|
*
|
* @param ies List of IEs to retrieve from
|
* @return {@link RoamingConsortium}
|
*/
|
public static RoamingConsortium getRoamingConsortiumIE(InformationElement[] ies) {
|
RoamingConsortium roamingConsortium = new RoamingConsortium();
|
if (ies != null) {
|
for (InformationElement ie : ies) {
|
if (ie.id == InformationElement.EID_ROAMING_CONSORTIUM) {
|
try {
|
roamingConsortium.from(ie);
|
} catch (RuntimeException e) {
|
Log.e(TAG, "Failed to parse Roaming Consortium IE: " + e.getMessage());
|
}
|
}
|
}
|
}
|
return roamingConsortium;
|
}
|
|
/**
|
* Parse and retrieve the Hotspot 2.0 Vendor Specific Information Element from the list of IEs.
|
*
|
* @param ies List of IEs to retrieve from
|
* @return {@link Vsa}
|
*/
|
public static Vsa getHS2VendorSpecificIE(InformationElement[] ies) {
|
Vsa vsa = new Vsa();
|
if (ies != null) {
|
for (InformationElement ie : ies) {
|
if (ie.id == InformationElement.EID_VSA) {
|
try {
|
vsa.from(ie);
|
} catch (RuntimeException e) {
|
Log.e(TAG, "Failed to parse Vendor Specific IE: " + e.getMessage());
|
}
|
}
|
}
|
}
|
return vsa;
|
}
|
|
/**
|
* Parse and retrieve the Interworking information element from the list of IEs.
|
*
|
* @param ies List of IEs to retrieve from
|
* @return {@link Interworking}
|
*/
|
public static Interworking getInterworkingIE(InformationElement[] ies) {
|
Interworking interworking = new Interworking();
|
if (ies != null) {
|
for (InformationElement ie : ies) {
|
if (ie.id == InformationElement.EID_INTERWORKING) {
|
try {
|
interworking.from(ie);
|
} catch (RuntimeException e) {
|
Log.e(TAG, "Failed to parse Interworking IE: " + e.getMessage());
|
}
|
}
|
}
|
}
|
return interworking;
|
}
|
|
public static class BssLoad {
|
public int stationCount = 0;
|
public int channelUtilization = 0;
|
public int capacity = 0;
|
|
public void from(InformationElement ie) {
|
if (ie.id != InformationElement.EID_BSS_LOAD) {
|
throw new IllegalArgumentException("Element id is not BSS_LOAD, : " + ie.id);
|
}
|
if (ie.bytes.length != 5) {
|
throw new IllegalArgumentException("BSS Load element length is not 5: "
|
+ ie.bytes.length);
|
}
|
ByteBuffer data = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
stationCount = data.getShort() & Constants.SHORT_MASK;
|
channelUtilization = data.get() & Constants.BYTE_MASK;
|
capacity = data.getShort() & Constants.SHORT_MASK;
|
}
|
}
|
|
public static class HtOperation {
|
public int secondChannelOffset = 0;
|
|
public int getChannelWidth() {
|
if (secondChannelOffset != 0) {
|
return 1;
|
} else {
|
return 0;
|
}
|
}
|
|
public int getCenterFreq0(int primaryFrequency) {
|
//40 MHz
|
if (secondChannelOffset != 0) {
|
if (secondChannelOffset == 1) {
|
return primaryFrequency + 10;
|
} else if (secondChannelOffset == 3) {
|
return primaryFrequency - 10;
|
} else {
|
Log.e("HtOperation", "Error on secondChannelOffset: " + secondChannelOffset);
|
return 0;
|
}
|
} else {
|
return 0;
|
}
|
}
|
|
public void from(InformationElement ie) {
|
if (ie.id != InformationElement.EID_HT_OPERATION) {
|
throw new IllegalArgumentException("Element id is not HT_OPERATION, : " + ie.id);
|
}
|
secondChannelOffset = ie.bytes[1] & 0x3;
|
}
|
}
|
|
public static class VhtOperation {
|
public int channelMode = 0;
|
public int centerFreqIndex1 = 0;
|
public int centerFreqIndex2 = 0;
|
|
public boolean isValid() {
|
return channelMode != 0;
|
}
|
|
public int getChannelWidth() {
|
return channelMode + 1;
|
}
|
|
public int getCenterFreq0() {
|
//convert channel index to frequency in MHz, channel 36 is 5180MHz
|
return (centerFreqIndex1 - 36) * 5 + 5180;
|
}
|
|
public int getCenterFreq1() {
|
if (channelMode > 1) { //160MHz
|
return (centerFreqIndex2 - 36) * 5 + 5180;
|
} else {
|
return 0;
|
}
|
}
|
|
public void from(InformationElement ie) {
|
if (ie.id != InformationElement.EID_VHT_OPERATION) {
|
throw new IllegalArgumentException("Element id is not VHT_OPERATION, : " + ie.id);
|
}
|
channelMode = ie.bytes[0] & Constants.BYTE_MASK;
|
centerFreqIndex1 = ie.bytes[1] & Constants.BYTE_MASK;
|
centerFreqIndex2 = ie.bytes[2] & Constants.BYTE_MASK;
|
}
|
}
|
|
public static class Interworking {
|
public NetworkDetail.Ant ant = null;
|
public boolean internet = false;
|
public long hessid = 0L;
|
|
public void from(InformationElement ie) {
|
if (ie.id != InformationElement.EID_INTERWORKING) {
|
throw new IllegalArgumentException("Element id is not INTERWORKING, : " + ie.id);
|
}
|
ByteBuffer data = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
int anOptions = data.get() & Constants.BYTE_MASK;
|
ant = NetworkDetail.Ant.values()[anOptions & 0x0f];
|
internet = (anOptions & 0x10) != 0;
|
// There are only three possible lengths for the Interworking IE:
|
// Len 1: Access Network Options only
|
// Len 3: Access Network Options & Venue Info
|
// Len 7: Access Network Options & HESSID
|
// Len 9: Access Network Options, Venue Info, & HESSID
|
if (ie.bytes.length != 1
|
&& ie.bytes.length != 3
|
&& ie.bytes.length != 7
|
&& ie.bytes.length != 9) {
|
throw new IllegalArgumentException(
|
"Bad Interworking element length: " + ie.bytes.length);
|
}
|
|
if (ie.bytes.length == 3 || ie.bytes.length == 9) {
|
int venueInfo = (int) ByteBufferReader.readInteger(data, ByteOrder.BIG_ENDIAN, 2);
|
}
|
|
if (ie.bytes.length == 7 || ie.bytes.length == 9) {
|
hessid = ByteBufferReader.readInteger(data, ByteOrder.BIG_ENDIAN, 6);
|
}
|
}
|
}
|
|
public static class RoamingConsortium {
|
public int anqpOICount = 0;
|
|
private long[] roamingConsortiums = null;
|
|
public long[] getRoamingConsortiums() {
|
return roamingConsortiums;
|
}
|
|
public void from(InformationElement ie) {
|
if (ie.id != InformationElement.EID_ROAMING_CONSORTIUM) {
|
throw new IllegalArgumentException("Element id is not ROAMING_CONSORTIUM, : "
|
+ ie.id);
|
}
|
ByteBuffer data = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
anqpOICount = data.get() & Constants.BYTE_MASK;
|
|
int oi12Length = data.get() & Constants.BYTE_MASK;
|
int oi1Length = oi12Length & Constants.NIBBLE_MASK;
|
int oi2Length = (oi12Length >>> 4) & Constants.NIBBLE_MASK;
|
int oi3Length = ie.bytes.length - 2 - oi1Length - oi2Length;
|
int oiCount = 0;
|
if (oi1Length > 0) {
|
oiCount++;
|
if (oi2Length > 0) {
|
oiCount++;
|
if (oi3Length > 0) {
|
oiCount++;
|
}
|
}
|
}
|
roamingConsortiums = new long[oiCount];
|
if (oi1Length > 0 && roamingConsortiums.length > 0) {
|
roamingConsortiums[0] =
|
ByteBufferReader.readInteger(data, ByteOrder.BIG_ENDIAN, oi1Length);
|
}
|
if (oi2Length > 0 && roamingConsortiums.length > 1) {
|
roamingConsortiums[1] =
|
ByteBufferReader.readInteger(data, ByteOrder.BIG_ENDIAN, oi2Length);
|
}
|
if (oi3Length > 0 && roamingConsortiums.length > 2) {
|
roamingConsortiums[2] =
|
ByteBufferReader.readInteger(data, ByteOrder.BIG_ENDIAN, oi3Length);
|
}
|
}
|
}
|
|
public static class Vsa {
|
private static final int ANQP_DOMID_BIT = 0x04;
|
|
public NetworkDetail.HSRelease hsRelease = null;
|
public int anqpDomainID = 0; // No domain ID treated the same as a 0; unique info per AP.
|
|
public void from(InformationElement ie) {
|
ByteBuffer data = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
if (ie.bytes.length >= 5 && data.getInt() == Constants.HS20_FRAME_PREFIX) {
|
int hsConf = data.get() & Constants.BYTE_MASK;
|
switch ((hsConf >> 4) & Constants.NIBBLE_MASK) {
|
case 0:
|
hsRelease = NetworkDetail.HSRelease.R1;
|
break;
|
case 1:
|
hsRelease = NetworkDetail.HSRelease.R2;
|
break;
|
default:
|
hsRelease = NetworkDetail.HSRelease.Unknown;
|
break;
|
}
|
if ((hsConf & ANQP_DOMID_BIT) != 0) {
|
if (ie.bytes.length < 7) {
|
throw new IllegalArgumentException(
|
"HS20 indication element too short: " + ie.bytes.length);
|
}
|
anqpDomainID = data.getShort() & Constants.SHORT_MASK;
|
}
|
}
|
}
|
}
|
|
/**
|
* This IE contained a bit field indicating the capabilities being advertised by the STA.
|
* The size of the bit field (number of bytes) is indicated by the |Length| field in the IE.
|
*
|
* Refer to Section 8.4.2.29 in IEEE 802.11-2012 Spec for capability associated with each
|
* bit.
|
*
|
* Here is the wire format of this IE:
|
* | Element ID | Length | Capabilities |
|
* 1 1 n
|
*/
|
public static class ExtendedCapabilities {
|
private static final int RTT_RESP_ENABLE_BIT = 70;
|
private static final int SSID_UTF8_BIT = 48;
|
|
public BitSet capabilitiesBitSet;
|
|
/**
|
* @return true if SSID should be interpreted using UTF-8 encoding
|
*/
|
public boolean isStrictUtf8() {
|
return capabilitiesBitSet.get(SSID_UTF8_BIT);
|
}
|
|
/**
|
* @return true if 802.11 MC RTT Response is enabled
|
*/
|
public boolean is80211McRTTResponder() {
|
return capabilitiesBitSet.get(RTT_RESP_ENABLE_BIT);
|
}
|
|
public ExtendedCapabilities() {
|
capabilitiesBitSet = new BitSet();
|
}
|
|
public ExtendedCapabilities(ExtendedCapabilities other) {
|
capabilitiesBitSet = other.capabilitiesBitSet;
|
}
|
|
/**
|
* Parse an ExtendedCapabilities from the IE containing raw bytes.
|
*
|
* @param ie The Information element data
|
*/
|
public void from(InformationElement ie) {
|
capabilitiesBitSet = BitSet.valueOf(ie.bytes);
|
}
|
}
|
|
/**
|
* parse beacon to build the capabilities
|
*
|
* This class is used to build the capabilities string of the scan results coming
|
* from HAL. It parses the ieee beacon's capability field, WPA and RSNE IE as per spec,
|
* and builds the ScanResult.capabilities String in a way that mirrors the values returned
|
* by wpa_supplicant.
|
*/
|
public static class Capabilities {
|
private static final int CAP_ESS_BIT_OFFSET = 0;
|
private static final int CAP_PRIVACY_BIT_OFFSET = 4;
|
|
private static final int WPA_VENDOR_OUI_TYPE_ONE = 0x01f25000;
|
private static final int WPS_VENDOR_OUI_TYPE = 0x04f25000;
|
private static final short WPA_VENDOR_OUI_VERSION = 0x0001;
|
private static final int OWE_VENDOR_OUI_TYPE = 0x1c9a6f50;
|
private static final short RSNE_VERSION = 0x0001;
|
|
private static final int WPA_AKM_EAP = 0x01f25000;
|
private static final int WPA_AKM_PSK = 0x02f25000;
|
|
private static final int RSN_AKM_EAP = 0x01ac0f00;
|
private static final int RSN_AKM_PSK = 0x02ac0f00;
|
private static final int RSN_AKM_FT_EAP = 0x03ac0f00;
|
private static final int RSN_AKM_FT_PSK = 0x04ac0f00;
|
private static final int RSN_AKM_EAP_SHA256 = 0x05ac0f00;
|
private static final int RSN_AKM_PSK_SHA256 = 0x06ac0f00;
|
private static final int RSN_AKM_SAE = 0x08ac0f00;
|
private static final int RSN_AKM_FT_SAE = 0x09ac0f00;
|
private static final int RSN_AKM_OWE = 0x12ac0f00;
|
private static final int RSN_AKM_EAP_SUITE_B_192 = 0x0cac0f00;
|
|
private static final int WPA_CIPHER_NONE = 0x00f25000;
|
private static final int WPA_CIPHER_TKIP = 0x02f25000;
|
private static final int WPA_CIPHER_CCMP = 0x04f25000;
|
|
private static final int RSN_CIPHER_NONE = 0x00ac0f00;
|
private static final int RSN_CIPHER_TKIP = 0x02ac0f00;
|
private static final int RSN_CIPHER_CCMP = 0x04ac0f00;
|
private static final int RSN_CIPHER_NO_GROUP_ADDRESSED = 0x07ac0f00;
|
private static final int RSN_CIPHER_GCMP_256 = 0x09ac0f00;
|
|
public ArrayList<Integer> protocol;
|
public ArrayList<ArrayList<Integer>> keyManagement;
|
public ArrayList<ArrayList<Integer>> pairwiseCipher;
|
public ArrayList<Integer> groupCipher;
|
public boolean isESS;
|
public boolean isPrivacy;
|
public boolean isWPS;
|
|
public Capabilities() {
|
}
|
|
// RSNE format (size unit: byte)
|
//
|
// | Element ID | Length | Version | Group Data Cipher Suite |
|
// 1 1 2 4
|
// | Pairwise Cipher Suite Count | Pairwise Cipher Suite List |
|
// 2 4 * m
|
// | AKM Suite Count | AKM Suite List | RSN Capabilities |
|
// 2 4 * n 2
|
// | PMKID Count | PMKID List | Group Management Cipher Suite |
|
// 2 16 * s 4
|
//
|
// Note: InformationElement.bytes has 'Element ID' and 'Length'
|
// stripped off already
|
private void parseRsnElement(InformationElement ie) {
|
ByteBuffer buf = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
|
try {
|
// version
|
if (buf.getShort() != RSNE_VERSION) {
|
// incorrect version
|
return;
|
}
|
|
// found the RSNE IE, hence start building the capability string
|
protocol.add(ScanResult.PROTOCOL_RSN);
|
|
// group data cipher suite
|
groupCipher.add(parseRsnCipher(buf.getInt()));
|
|
// pairwise cipher suite count
|
short cipherCount = buf.getShort();
|
ArrayList<Integer> rsnPairwiseCipher = new ArrayList<>();
|
// pairwise cipher suite list
|
for (int i = 0; i < cipherCount; i++) {
|
rsnPairwiseCipher.add(parseRsnCipher(buf.getInt()));
|
}
|
pairwiseCipher.add(rsnPairwiseCipher);
|
|
// AKM
|
// AKM suite count
|
short akmCount = buf.getShort();
|
ArrayList<Integer> rsnKeyManagement = new ArrayList<>();
|
|
for (int i = 0; i < akmCount; i++) {
|
int akm = buf.getInt();
|
switch (akm) {
|
case RSN_AKM_EAP:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_EAP);
|
break;
|
case RSN_AKM_PSK:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_PSK);
|
break;
|
case RSN_AKM_FT_EAP:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_FT_EAP);
|
break;
|
case RSN_AKM_FT_PSK:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_FT_PSK);
|
break;
|
case RSN_AKM_EAP_SHA256:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_EAP_SHA256);
|
break;
|
case RSN_AKM_PSK_SHA256:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_PSK_SHA256);
|
break;
|
case RSN_AKM_SAE:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_SAE);
|
break;
|
case RSN_AKM_FT_SAE:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_FT_SAE);
|
break;
|
case RSN_AKM_OWE:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_OWE);
|
break;
|
case RSN_AKM_EAP_SUITE_B_192:
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_EAP_SUITE_B_192);
|
break;
|
default:
|
// do nothing
|
break;
|
}
|
}
|
// Default AKM
|
if (rsnKeyManagement.isEmpty()) {
|
rsnKeyManagement.add(ScanResult.KEY_MGMT_EAP);
|
}
|
keyManagement.add(rsnKeyManagement);
|
} catch (BufferUnderflowException e) {
|
Log.e("IE_Capabilities", "Couldn't parse RSNE, buffer underflow");
|
}
|
}
|
|
private static int parseWpaCipher(int cipher) {
|
switch (cipher) {
|
case WPA_CIPHER_NONE:
|
return ScanResult.CIPHER_NONE;
|
case WPA_CIPHER_TKIP:
|
return ScanResult.CIPHER_TKIP;
|
case WPA_CIPHER_CCMP:
|
return ScanResult.CIPHER_CCMP;
|
default:
|
Log.w("IE_Capabilities", "Unknown WPA cipher suite: "
|
+ Integer.toHexString(cipher));
|
return ScanResult.CIPHER_NONE;
|
}
|
}
|
|
private static int parseRsnCipher(int cipher) {
|
switch (cipher) {
|
case RSN_CIPHER_NONE:
|
return ScanResult.CIPHER_NONE;
|
case RSN_CIPHER_TKIP:
|
return ScanResult.CIPHER_TKIP;
|
case RSN_CIPHER_CCMP:
|
return ScanResult.CIPHER_CCMP;
|
case RSN_CIPHER_GCMP_256:
|
return ScanResult.CIPHER_GCMP_256;
|
case RSN_CIPHER_NO_GROUP_ADDRESSED:
|
return ScanResult.CIPHER_NO_GROUP_ADDRESSED;
|
default:
|
Log.w("IE_Capabilities", "Unknown RSN cipher suite: "
|
+ Integer.toHexString(cipher));
|
return ScanResult.CIPHER_NONE;
|
}
|
}
|
|
private static boolean isWpsElement(InformationElement ie) {
|
ByteBuffer buf = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
try {
|
// WPS OUI and type
|
return (buf.getInt() == WPS_VENDOR_OUI_TYPE);
|
} catch (BufferUnderflowException e) {
|
Log.e("IE_Capabilities", "Couldn't parse VSA IE, buffer underflow");
|
return false;
|
}
|
}
|
|
private static boolean isWpaOneElement(InformationElement ie) {
|
ByteBuffer buf = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
|
try {
|
// WPA OUI and type
|
return (buf.getInt() == WPA_VENDOR_OUI_TYPE_ONE);
|
} catch (BufferUnderflowException e) {
|
Log.e("IE_Capabilities", "Couldn't parse VSA IE, buffer underflow");
|
return false;
|
}
|
}
|
|
// WPA type 1 format (size unit: byte)
|
//
|
// | Element ID | Length | OUI | Type | Version |
|
// 1 1 3 1 2
|
// | Group Data Cipher Suite |
|
// 4
|
// | Pairwise Cipher Suite Count | Pairwise Cipher Suite List |
|
// 2 4 * m
|
// | AKM Suite Count | AKM Suite List |
|
// 2 4 * n
|
//
|
// Note: InformationElement.bytes has 'Element ID' and 'Length'
|
// stripped off already
|
//
|
private void parseWpaOneElement(InformationElement ie) {
|
ByteBuffer buf = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
|
try {
|
// skip WPA OUI and type parsing. isWpaOneElement() should have
|
// been called for verification before we reach here.
|
buf.getInt();
|
|
// version
|
if (buf.getShort() != WPA_VENDOR_OUI_VERSION) {
|
// incorrect version
|
return;
|
}
|
|
// start building the string
|
protocol.add(ScanResult.PROTOCOL_WPA);
|
|
// group data cipher suite
|
groupCipher.add(parseWpaCipher(buf.getInt()));
|
|
// pairwise cipher suite count
|
short cipherCount = buf.getShort();
|
ArrayList<Integer> wpaPairwiseCipher = new ArrayList<>();
|
// pairwise chipher suite list
|
for (int i = 0; i < cipherCount; i++) {
|
wpaPairwiseCipher.add(parseWpaCipher(buf.getInt()));
|
}
|
pairwiseCipher.add(wpaPairwiseCipher);
|
|
// AKM
|
// AKM suite count
|
short akmCount = buf.getShort();
|
ArrayList<Integer> wpaKeyManagement = new ArrayList<>();
|
|
// AKM suite list
|
for (int i = 0; i < akmCount; i++) {
|
int akm = buf.getInt();
|
switch (akm) {
|
case WPA_AKM_EAP:
|
wpaKeyManagement.add(ScanResult.KEY_MGMT_EAP);
|
break;
|
case WPA_AKM_PSK:
|
wpaKeyManagement.add(ScanResult.KEY_MGMT_PSK);
|
break;
|
default:
|
// do nothing
|
break;
|
}
|
}
|
// Default AKM
|
if (wpaKeyManagement.isEmpty()) {
|
wpaKeyManagement.add(ScanResult.KEY_MGMT_EAP);
|
}
|
keyManagement.add(wpaKeyManagement);
|
} catch (BufferUnderflowException e) {
|
Log.e("IE_Capabilities", "Couldn't parse type 1 WPA, buffer underflow");
|
}
|
}
|
|
/**
|
* Parse the Information Element and the 16-bit Capability Information field
|
* to build the InformationElemmentUtil.capabilities object.
|
*
|
* @param ies -- Information Element array
|
* @param beaconCap -- 16-bit Beacon Capability Information field
|
* @param isOweSupported -- Boolean flag to indicate if OWE is supported by the device
|
*/
|
|
public void from(InformationElement[] ies, BitSet beaconCap, boolean isOweSupported) {
|
protocol = new ArrayList<Integer>();
|
keyManagement = new ArrayList<ArrayList<Integer>>();
|
groupCipher = new ArrayList<Integer>();
|
pairwiseCipher = new ArrayList<ArrayList<Integer>>();
|
|
if (ies == null || beaconCap == null) {
|
return;
|
}
|
isESS = beaconCap.get(CAP_ESS_BIT_OFFSET);
|
isPrivacy = beaconCap.get(CAP_PRIVACY_BIT_OFFSET);
|
for (InformationElement ie : ies) {
|
if (ie.id == InformationElement.EID_RSN) {
|
parseRsnElement(ie);
|
}
|
|
if (ie.id == InformationElement.EID_VSA) {
|
if (isWpaOneElement(ie)) {
|
parseWpaOneElement(ie);
|
}
|
if (isWpsElement(ie)) {
|
// TODO(b/62134557): parse WPS IE to provide finer granularity information.
|
isWPS = true;
|
}
|
if (isOweSupported && isOweElement(ie)) {
|
/* From RFC 8110: Once the client and AP have finished 802.11 association,
|
they then complete the Diffie-Hellman key exchange and create a Pairwise
|
Master Key (PMK) and its associated identifier, PMKID [IEEE802.11].
|
Upon completion of 802.11 association, the AP initiates the 4-way
|
handshake to the client using the PMK generated above. The 4-way
|
handshake generates a Key-Encrypting Key (KEK), a Key-Confirmation
|
Key (KCK), and a Message Integrity Code (MIC) to use for protection
|
of the frames that define the 4-way handshake.
|
|
We check if OWE is supported here because we are adding the OWE
|
capabilities to the Open BSS. Non-supporting devices need to see this
|
open network and ignore this element. Supporting devices need to hide
|
the Open BSS of OWE in transition mode and connect to the Hidden one.
|
*/
|
protocol.add(ScanResult.PROTOCOL_RSN);
|
groupCipher.add(ScanResult.CIPHER_CCMP);
|
ArrayList<Integer> owePairwiseCipher = new ArrayList<>();
|
owePairwiseCipher.add(ScanResult.CIPHER_CCMP);
|
pairwiseCipher.add(owePairwiseCipher);
|
ArrayList<Integer> oweKeyManagement = new ArrayList<>();
|
oweKeyManagement.add(ScanResult.KEY_MGMT_OWE_TRANSITION);
|
keyManagement.add(oweKeyManagement);
|
}
|
}
|
}
|
}
|
|
private static boolean isOweElement(InformationElement ie) {
|
ByteBuffer buf = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
try {
|
// OWE OUI and type
|
return (buf.getInt() == OWE_VENDOR_OUI_TYPE);
|
} catch (BufferUnderflowException e) {
|
Log.e("IE_Capabilities", "Couldn't parse VSA IE, buffer underflow");
|
return false;
|
}
|
}
|
|
private String protocolToString(int protocol) {
|
switch (protocol) {
|
case ScanResult.PROTOCOL_NONE:
|
return "None";
|
case ScanResult.PROTOCOL_WPA:
|
return "WPA";
|
case ScanResult.PROTOCOL_RSN:
|
return "RSN";
|
default:
|
return "?";
|
}
|
}
|
|
private String keyManagementToString(int akm) {
|
switch (akm) {
|
case ScanResult.KEY_MGMT_NONE:
|
return "None";
|
case ScanResult.KEY_MGMT_PSK:
|
return "PSK";
|
case ScanResult.KEY_MGMT_EAP:
|
return "EAP";
|
case ScanResult.KEY_MGMT_FT_EAP:
|
return "FT/EAP";
|
case ScanResult.KEY_MGMT_FT_PSK:
|
return "FT/PSK";
|
case ScanResult.KEY_MGMT_EAP_SHA256:
|
return "EAP-SHA256";
|
case ScanResult.KEY_MGMT_PSK_SHA256:
|
return "PSK-SHA256";
|
case ScanResult.KEY_MGMT_OWE:
|
return "OWE";
|
case ScanResult.KEY_MGMT_OWE_TRANSITION:
|
return "OWE_TRANSITION";
|
case ScanResult.KEY_MGMT_SAE:
|
return "SAE";
|
case ScanResult.KEY_MGMT_FT_SAE:
|
return "FT/SAE";
|
case ScanResult.KEY_MGMT_EAP_SUITE_B_192:
|
return "EAP_SUITE_B_192";
|
default:
|
return "?";
|
}
|
}
|
|
private String cipherToString(int cipher) {
|
switch (cipher) {
|
case ScanResult.CIPHER_NONE:
|
return "None";
|
case ScanResult.CIPHER_CCMP:
|
return "CCMP";
|
case ScanResult.CIPHER_GCMP_256:
|
return "GCMP-256";
|
case ScanResult.CIPHER_TKIP:
|
return "TKIP";
|
default:
|
return "?";
|
}
|
}
|
|
/**
|
* Build the ScanResult.capabilities String.
|
*
|
* @return security string that mirrors what wpa_supplicant generates
|
*/
|
public String generateCapabilitiesString() {
|
StringBuilder capabilities = new StringBuilder();
|
// private Beacon without an RSNE or WPA IE, hence WEP0
|
boolean isWEP = (protocol.isEmpty()) && isPrivacy;
|
|
if (isWEP) {
|
capabilities.append("[WEP]");
|
}
|
for (int i = 0; i < protocol.size(); i++) {
|
String capability = generateCapabilitiesStringPerProtocol(i);
|
// add duplicate capabilities for WPA2 for backward compatibility:
|
// duplicate "RSN" entries as "WPA2"
|
String capWpa2 = generateWPA2CapabilitiesString(capability, i);
|
capabilities.append(capWpa2);
|
capabilities.append(capability);
|
}
|
if (isESS) {
|
capabilities.append("[ESS]");
|
}
|
if (isWPS) {
|
capabilities.append("[WPS]");
|
}
|
|
return capabilities.toString();
|
}
|
|
/**
|
* Build the Capability String for one protocol
|
* @param index: index number of the protocol
|
* @return security string for one protocol
|
*/
|
private String generateCapabilitiesStringPerProtocol(int index) {
|
StringBuilder capability = new StringBuilder();
|
capability.append("[").append(protocolToString(protocol.get(index)));
|
|
if (index < keyManagement.size()) {
|
for (int j = 0; j < keyManagement.get(index).size(); j++) {
|
capability.append((j == 0) ? "-" : "+").append(
|
keyManagementToString(keyManagement.get(index).get(j)));
|
}
|
}
|
if (index < pairwiseCipher.size()) {
|
for (int j = 0; j < pairwiseCipher.get(index).size(); j++) {
|
capability.append((j == 0) ? "-" : "+").append(
|
cipherToString(pairwiseCipher.get(index).get(j)));
|
}
|
}
|
capability.append("]");
|
return capability.toString();
|
}
|
|
/**
|
* Build the duplicate Capability String for WPA2
|
* @param cap: original capability String
|
* @param index: index number of the protocol
|
* @return security string for WPA2, empty String if protocol is not WPA2
|
*/
|
private String generateWPA2CapabilitiesString(String cap, int index) {
|
StringBuilder capWpa2 = new StringBuilder();
|
// if not WPA2, return empty String
|
if (cap.contains("EAP_SUITE_B_192")
|
|| (!cap.contains("RSN-EAP") && !cap.contains("RSN-FT/EAP")
|
&& !cap.contains("RSN-PSK") && !cap.contains("RSN-FT/PSK"))) {
|
return "";
|
}
|
capWpa2.append("[").append("WPA2");
|
if (index < keyManagement.size()) {
|
for (int j = 0; j < keyManagement.get(index).size(); j++) {
|
capWpa2.append((j == 0) ? "-" : "+").append(
|
keyManagementToString(keyManagement.get(index).get(j)));
|
// WPA3/WPA2 transition mode
|
if (cap.contains("SAE")) {
|
break;
|
}
|
}
|
}
|
if (index < pairwiseCipher.size()) {
|
for (int j = 0; j < pairwiseCipher.get(index).size(); j++) {
|
capWpa2.append((j == 0) ? "-" : "+").append(
|
cipherToString(pairwiseCipher.get(index).get(j)));
|
}
|
}
|
capWpa2.append("]");
|
return capWpa2.toString();
|
}
|
}
|
|
|
/**
|
* Parser for the Traffic Indication Map (TIM) Information Element (EID 5). This element will
|
* only be present in scan results that are derived from a Beacon Frame, not from the more
|
* plentiful probe responses. Call 'isValid()' after parsing, to ensure the results are correct.
|
*/
|
public static class TrafficIndicationMap {
|
private static final int MAX_TIM_LENGTH = 254;
|
private boolean mValid = false;
|
public int mLength = 0;
|
public int mDtimCount = -1;
|
//Negative DTIM Period means no TIM element was given this frame.
|
public int mDtimPeriod = -1;
|
public int mBitmapControl = 0;
|
|
/**
|
* Is this a valid TIM information element.
|
*/
|
public boolean isValid() {
|
return mValid;
|
}
|
|
// Traffic Indication Map format (size unit: byte)
|
//
|
//| ElementID | Length | DTIM Count | DTIM Period | BitmapControl | Partial Virtual Bitmap |
|
// 1 1 1 1 1 1 - 251
|
//
|
// Note: InformationElement.bytes has 'Element ID' and 'Length'
|
// stripped off already
|
//
|
public void from(InformationElement ie) {
|
mValid = false;
|
if (ie == null || ie.bytes == null) return;
|
mLength = ie.bytes.length;
|
ByteBuffer data = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
try {
|
mDtimCount = data.get() & Constants.BYTE_MASK;
|
mDtimPeriod = data.get() & Constants.BYTE_MASK;
|
mBitmapControl = data.get() & Constants.BYTE_MASK;
|
//A valid TIM element must have atleast one more byte
|
data.get();
|
} catch (BufferUnderflowException e) {
|
return;
|
}
|
if (mLength <= MAX_TIM_LENGTH && mDtimPeriod > 0) {
|
mValid = true;
|
}
|
}
|
}
|
|
/**
|
* This util class determines the 802.11 standard (a/b/g/n/ac) being used
|
*/
|
public static class WifiMode {
|
public static final int MODE_UNDEFINED = 0; // Unknown/undefined
|
public static final int MODE_11A = 1; // 802.11a
|
public static final int MODE_11B = 2; // 802.11b
|
public static final int MODE_11G = 3; // 802.11g
|
public static final int MODE_11N = 4; // 802.11n
|
public static final int MODE_11AC = 5; // 802.11ac
|
//<TODO> add support for 802.11ad and be more selective instead of defaulting to 11A
|
|
/**
|
* Use frequency, max supported rate, and the existence of VHT, HT & ERP fields in scan
|
* scan result to determine the 802.11 Wifi standard being used.
|
*/
|
public static int determineMode(int frequency, int maxRate, boolean foundVht,
|
boolean foundHt, boolean foundErp) {
|
if (foundVht) {
|
return MODE_11AC;
|
} else if (foundHt) {
|
return MODE_11N;
|
} else if (foundErp) {
|
return MODE_11G;
|
} else if (frequency < 3000) {
|
if (maxRate < 24000000) {
|
return MODE_11B;
|
} else {
|
return MODE_11G;
|
}
|
} else {
|
return MODE_11A;
|
}
|
}
|
|
/**
|
* Map the wifiMode integer to its type, and output as String MODE_11<A/B/G/N/AC>
|
*/
|
public static String toString(int mode) {
|
switch(mode) {
|
case MODE_11A:
|
return "MODE_11A";
|
case MODE_11B:
|
return "MODE_11B";
|
case MODE_11G:
|
return "MODE_11G";
|
case MODE_11N:
|
return "MODE_11N";
|
case MODE_11AC:
|
return "MODE_11AC";
|
default:
|
return "MODE_UNDEFINED";
|
}
|
}
|
}
|
|
/**
|
* Parser for both the Supported Rates & Extended Supported Rates Information Elements
|
*/
|
public static class SupportedRates {
|
public static final int MASK = 0x7F; // 0111 1111
|
public boolean mValid = false;
|
public ArrayList<Integer> mRates;
|
|
public SupportedRates() {
|
mRates = new ArrayList<Integer>();
|
}
|
|
/**
|
* Is this a valid Supported Rates information element.
|
*/
|
public boolean isValid() {
|
return mValid;
|
}
|
|
/**
|
* get the Rate in bits/s from associated byteval
|
*/
|
public static int getRateFromByte(int byteVal) {
|
byteVal &= MASK;
|
switch(byteVal) {
|
case 2:
|
return 1000000;
|
case 4:
|
return 2000000;
|
case 11:
|
return 5500000;
|
case 12:
|
return 6000000;
|
case 18:
|
return 9000000;
|
case 22:
|
return 11000000;
|
case 24:
|
return 12000000;
|
case 36:
|
return 18000000;
|
case 44:
|
return 22000000;
|
case 48:
|
return 24000000;
|
case 66:
|
return 33000000;
|
case 72:
|
return 36000000;
|
case 96:
|
return 48000000;
|
case 108:
|
return 54000000;
|
default:
|
//ERROR UNKNOWN RATE
|
return -1;
|
}
|
}
|
|
// Supported Rates format (size unit: byte)
|
//
|
//| ElementID | Length | Supported Rates [7 Little Endian Info bits - 1 Flag bit]
|
// 1 1 1 - 8
|
//
|
// Note: InformationElement.bytes has 'Element ID' and 'Length'
|
// stripped off already
|
//
|
public void from(InformationElement ie) {
|
mValid = false;
|
if (ie == null || ie.bytes == null || ie.bytes.length > 8 || ie.bytes.length < 1) {
|
return;
|
}
|
ByteBuffer data = ByteBuffer.wrap(ie.bytes).order(ByteOrder.LITTLE_ENDIAN);
|
try {
|
for (int i = 0; i < ie.bytes.length; i++) {
|
int rate = getRateFromByte(data.get());
|
if (rate > 0) {
|
mRates.add(rate);
|
} else {
|
return;
|
}
|
}
|
} catch (BufferUnderflowException e) {
|
return;
|
}
|
mValid = true;
|
return;
|
}
|
|
/**
|
* Lists the rates in a human readable string
|
*/
|
public String toString() {
|
StringBuilder sbuf = new StringBuilder();
|
for (Integer rate : mRates) {
|
sbuf.append(String.format("%.1f", (double) rate / 1000000) + ", ");
|
}
|
return sbuf.toString();
|
}
|
}
|
}
|