文章参考的是Android 7.1的源码
本文主要研究EAP类型网络的身份验证过程,从而简化Android平台EAP类型网络的身份验证过程。
上篇文章Wi-Fi EAP网络验证过程与Android平台拓展实例(一)已经分析出IOS和Android平台在EAP类型网络认证上的差异,即提供给认证服务器的EAP方法不同。那么这篇文章就通过分析Android源码来大概了解下整个认证过程。
1. wpa_supplicant.conf
在看源码之前,我们先看看Android中AP配置信息的保存。在Android7.1及其以下的版本中,AP的基本配置信息,如身份,密码,加密类型等,都保存在data/misc/wifi/wpa_supplicant.conf
这个文件中,那么我们来看看连接TPLINK-TTLS(加密方式EAP-TTLS)的情况下,该AP的保存信息:
unknown:/ # cat data/misc/wifi/wpa_supplicant.conf
...
network={
ssid="TPLINK-TTLS"
key_mgmt=WPA-EAP IEEE8021X
eap=PEAP
identity="ulangch"
password="12345678"
phase2=""
priority=3
proactive_key_caching=1
disabled=1
id_str="%7B%22creatorUid%22%3A%221000%22%2C%22configKey%22%3A%22%5C%22TPLINK-TTLS%5C%22-WPA_EAP%22%7D"
}
如上所示,在我们选择了PEAP方法时,wpa_supplicant.conf
中保存了"eap=PEAP"
的信息,如果我们把该文件pull出来修改成"eap=TTLS"
push回去再重启Wi-Fi连接该AP,则也可以正常连上。
2. EAP网络-Framework
我们先看看EAP网络信息在framework中的保存过程是什么样的,如果大家对framework中AP的保存过程非常熟悉,建议跳过该节内容,直接查看wpa_supplicant中的部分。
WifiStateMachine在收到WifiService传递过来的SAVE_NETWORK
消息后,调用WifiConfigManager进行了保存操作:
/** WifiStateMachine.java*/
case WifiManager.SAVE_NETWORK:
case WifiStateMachine.CMD_AUTO_SAVE_NETWORK:
...
// 调用WifiConfigManager.saveNetwork来保存WifiConfiguration
result = mWifiConfigManager.saveNetwork(config, WifiConfiguration.UNKNOWN_UID);
...
WifiConfigManager开始进行保存和更新,经过一系列的检查,最终会使用WifiConfigStore来保存
/** WifiConfigManager.java*/
NetworkUpdateResult saveNetwork(WifiConfiguration config, int uid) {
...
NetworkUpdateResult result = addOrUpdateNetworkNative(config, uid);
}
/** WifiConfigManager.java*/
private NetworkUpdateResult addOrUpdateNetworkNative(WifiConfiguration config, int uid) {
...
if (!mWifiConfigStore.addOrUpdateNetwork(config, currentConfig)) {
return new NetworkUpdateResult(INVALID_NETWORK_ID);
}
}
WifiConfigStore在经过检查和保存AP的基本信息后,对于EAP相关的配置信息,如身份,密码,CA证书等会通过WifiEnterpriseConfig中提供的接口进行保存:
/**WifiConfigStore.java*/
public boolean addOrUpdateNetwork(WifiConfiguration config, WifiConfiguration existingConfig) {
...
if (!saveNetwork(config, netId)) {
if (newNetwork) {
mWifiNative.removeNetwork(netId);
loge("Failed to set a network variable, removed network: " + netId);
}
return false;
}
if (config.enterpriseConfig != null
&& config.enterpriseConfig.getEapMethod() != WifiEnterpriseConfig.Eap.NONE) {
return updateNetworkKeys(config, existingConfig);
}
}
/** WifiConfigStore.java*/
private boolean updateNetworkKeys(WifiConfiguration config, WifiConfiguration existingConfig) {
...
// WifiConfigStore#SupplicantSaver继承自WifiEnterpriseConfig#SupplicantSaver,
// 因此最终还是会回到WifiConfigStore中进行保存操作
if (!enterpriseConfig.saveToSupplicant(
new SupplicantSaver(config.networkId, config.SSID))) {
removeKeys(enterpriseConfig);
return false;
}
return true;
}
WifiEnterpriseConfig将实例中的EAP的属性信息保存:
/** WifiEnterpriseConfig.java*/
public boolean saveToSupplicant(SupplicantSaver saver) {
...
// 这里我们关心EAP方法的保存,其他属性的保存与eap method保存的方式是一样的
// Eap.strings[mEapMethod])返回当前mEapMethod对应的EAP方法字符串名称
// 前面说过,SupplicantSaver是WifiConfigStore.SupplicantSaver, 因此继续回到WifiConfigStore
if (!saver.saveValue(EAP_KEY, Eap.strings[mEapMethod])) {
return false;
}
}
从上面的"mEapMethod"
可以看出,framework中的EAP方法保存在这个变量中,对应一种类型的EAP方法。
下面是WifiConfigStore#SupplicantSaver调用WifiNative中提供的方法进行保存:
/** WifiConfigStore.java*/
private class SupplicantSaver implements WifiEnterpriseConfig.SupplicantSaver {
@Override
public boolean saveValue(String key, String value) {
if (!mWifiNative.setNetworkVariable(mNetId, key, value)) {
loge(mSetterSSID + ": failed to set " + key + ": " + value);
return false;
}
return true;
}
}
WifiNative中通过发送"SET_NETWORK"
命令到 wpa_supplicant
:
/** WifiNative.java*/
public boolean setNetworkVariable(int netId, String name, String value) {
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(value)) return false;
if (name.equals(WifiConfiguration.pskVarName)
|| name.equals(WifiEnterpriseConfig.PASSWORD_KEY)) {
return doBooleanCommandWithoutLogging("SET_NETWORK " + netId + " " + name + " " + value);
} else {
// EAP 方法最终使用 SET_NETWORK 0 eap PEAP 这种命令来实现
return doBooleanCommand("SET_NETWORK " + netId + " " + name + " " + value);
}
}
所以从上面的流程可以看出,在framework中,EAP属性信息缓存在WifiEnterpriseConfig中,而EAP方法保存在"mEapMethod"
这个整型变量中。当要进行连接或者保存时,都会通过 SET_NETWORK
指令通过将其配置信息设置到wpa_supplicant。同样的,我们可以直接在adb shell中使用wpa_cli
工具来修改EAP方法或者AP的其他信息。
3. EAP 网络-wpa_supplicant
至于system_server与wpa_supplicant,他们是通过建立socket来通信的,具体流程这里就不赘述了,我们直接跳转到wpa_supplicant中处理 "SET_NETWORK"
的相关逻辑,并且,我们只关心EAP部分EAP方法的配置内容,假设WifiNative的命令是 "SET_NETWORK 0 eap PEAP"
首先从wpa_supplicant socket server处理消息入手:
/** external/wpa_supplicant_8/wpa_supplicant/ctrl_iface.c*/
char * wpa_supplicant_ctrl_iface_process(struct wpa_supplicant *wpa_s,
char *buf, size_t *resp_len)
{
....
} else if (os_strncmp(buf, "SET_NETWORK ", 12) == 0) {
// buf + 12表示从 "network id" 开始
// 所以剩下的命令是" "
// 在我们的这个例子中就是 "0 eap PEAP"
if (wpa_supplicant_ctrl_iface_set_network(wpa_s, buf + 12))
reply_len = -1;
}
}
下面就是设定wpa_ssid
各个key-value的地方(wpa_ssid是wpa_supplicant中保存网络配置的一个结构体,类似于framework中的WifiConfiguration):
/** external/wpa_supplicant_8/wpa_supplicant/ctrl_iface.c*/
static int wpa_supplicant_ctrl_iface_set_network(
struct wpa_supplicant *wpa_s, char *cmd)
{
int id, ret, prev_bssid_set, prev_disabled;
struct wpa_ssid *ssid;
char *name, *value;
u8 prev_bssid[ETH_ALEN];
/* cmd: " " */
// 取出,注意这里的"name"还不是真正的name,
// 应该是除去 剩下的部分,具体可以研究下os_strchr方法
name = os_strchr(cmd, ' ');
if (name == NULL)
return -1;
// 这里的作用是为了正确的取出id
*name++ = '\0';
value = os_strchr(name, ' ');
if (value == NULL)
return -1;
// 这里的作用是为了正确取出name
*value++ = '\0';
id = atoi(cmd);
wpa_printf(MSG_DEBUG, "CTRL_IFACE: SET_NETWORK id=%d name='%s'",
id, name);
wpa_hexdump_ascii_key(MSG_DEBUG, "CTRL_IFACE: value",
(u8 *) value, os_strlen(value));
// 根据取出对应的wpa_ssid
ssid = wpa_config_get_network(wpa_s->conf, id);
if (ssid == NULL) {
wpa_printf(MSG_DEBUG, "CTRL_IFACE: Could not find network "
"id=%d", id);
return -1;
}
prev_bssid_set = ssid->bssid_set;
prev_disabled = ssid->disabled;
os_memcpy(prev_bssid, ssid->bssid, ETH_ALEN);
// 更新已保存的wpa_ssid,很类似WifiConfigManager中的addOrUpdateNetwork
ret = wpa_supplicant_ctrl_iface_update_network(wpa_s, ssid, name,
value);
}
/** external/wpa_supplicant_8/wpa_supplicant/ctrl_iface.c*/
static int wpa_supplicant_ctrl_iface_update_network(
struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid,
char *name, char *value)
{
int ret;
// 设置name & value
ret = wpa_config_set(ssid, name, value, 0);
...
}
所有的variable都会通过下面的"wpa_config_set"
方法进行不同的处理,对于不同的variable,config.c中采用了巧妙的方式:"#define _FUNC(f) #f, wpa_config_parse_ ## f, wpa_config_write_ ## f, NULL, NULL, NULL, NULL"
。如处理”eap”这个variable,就采用wpa_config_parse_eap
来处理:
/** external\wpa_supplicant_8\wpa_supplicant/config.c*/
int wpa_config_set(struct wpa_ssid *ssid, const char *var, const char *value,
int line)
{
...
for (i = 0; i < NUM_SSID_FIELDS; i++) {
const struct parse_data *field = &ssid_fields[i];
if (os_strcmp(var, field->name) != 0)
continue;
// 重点查看paser这个方法,在我们"SET_NETWORK 0 eap PEAP"这个例子中,
// parser对应方法应该是"wpa_config_parse_eap",对于不同的variable,
// config.c中有不同的函数去处理
ret = field->parser(field, ssid, line, value);
}
}
接下来就是我们重点要看的地方,如何将PEAP这个eap方法配置到wpa_ssid
中,从中,我们也可以获得一些启发:
/** external\wpa_supplicant_8\wpa_supplicant/config.c*/
static int wpa_config_parse_eap(const struct parse_data *data,
struct wpa_ssid *ssid, int line,
const char *value)
{
int last, errors = 0;
char *start, *end, *buf;
/** 从methods这个定义来看,就初见蹊跷,是否是支持多个method呢?*/
struct eap_method_type *methods = NULL, *tmp;
size_t num_methods = 0;
// 将value拷贝到buf中
buf = os_strdup(value);
if (buf == NULL)
return -1;
start = buf;
// 循环查找methods
while (*start != '\0') {
// 从这个地方,就已经可以确定,wpa_supplicant是支持多个eap method的,
// 多个eap 方法只需要使用' '或者'\t'分开就可以,
// 我们只需要类似使用"SET_NETWORK 0 eap PEAP TTLS"这种方式就可以
// 配置多个eap method,wpa_supplicant会对methods进行解析并且存储
while (*start == ' ' || *start == '\t')
start++;
if (*start == '\0')
break;
end = start;
while (*end != ' ' && *end != '\t' && *end != '\0')
end++;
last = *end == '\0';
*end = '\0';
tmp = methods;
// 每找到一个method,就增大methods的长度,
methods = os_realloc_array(methods, num_methods + 1,
sizeof(*methods));
...
// 校验设定的eap method是否是有效可支持的,wpa_supplicant中支持的eap方法
// 都可以在"external\wpa_supplicant_8\src\eap_common\eap_defs.h"中查看
methods[num_methods].method = eap_peer_get_type(
start, &methods[num_methods].vendor);
if (methods[num_methods].vendor == EAP_VENDOR_IETF &&
methods[num_methods].method == EAP_TYPE_NONE) {
wpa_printf(MSG_ERROR, "Line %d: unknown EAP method "
"'%s'", line, start);
...
errors++;
}
}
// 省略的部分是对wpa_ssid中存在的eap_methods和正在配置的eap_methods进行匹配,
// 如果能够完全匹配,则不需要重新配置,具体可以看源码,这里不做详解
...
// 直接将methods设置到network_id对应的wpa_ssid中
ssid->eap.eap_methods = methods;
return errors ? -1 : 0;
}
根据上面的分析,现在已经很清楚的知道,wpa_supplicant 中是支持配置多种eap method的,但是 framework 中却只配置一种方法,WifiEnterpriseConfig.java中只定义了一个"mEapMethod"
,为了进一步确认 wpa_supplicant 这个支持是否可行,我们接下来进行试验求证。
4. 实验求证
方案一:直接修改data/misc/wifi/wpa_supplicant.conf
中保存的network
// 1. 将wpa_supplicant.conf pull出来
xyzc@xyzc-ul:~/ulangch/wifi$ adb pull data/misc/wifi/wpa_supplicant.conf
8 KB/s (689 bytes in 0.083s)
/// 2. 修改wpa_supplicant.conf中对应的eap network:
// 将"eap=PEAP"修改成"eap=PEAP TTLS PWD":
network={
ssid="TPLINK-TTLS"
key_mgmt=WPA-EAP IEEE8021X
eap=PEAP TTLS PWD
identity="ulangch"
password="12345678"
phase2=""
priority=2
proactive_key_caching=1
disabled=1
id_str="%7B%22creatorUid%22%3A%221000%22%2C%22configKey%22%3A%22%5C%22TPLINK-TTLS%5C%22-WPA_EAP%22%7D"
}
// 3. 修改完成后push到原来的位置:
xyzc@xyzc-ul:~/ulangch/wifi$ adb push wpa_supplicant.conf data/misc/wifi
12 KB/s (689 bytes in 0.054s)
// 4. 开关WiFi重新进行连接
// 5. 结论:可以正常连接上EAP方法为TTLS的网络
方案二:使用wpa_cli工具修改 wpa_ssid
实现多个EAP方法验证
/// 1. 进入adb shell
xyzc@xyzc-ul:~/ulangch/wifi$ adb shell
// 2. 使用wpa_cli 工具
unknown:/ # wpa_cli
wpa_cli v2.6-devel-7.1.1
Copyright (c) 2004-2016, Jouni Malinen and contributors
This software may be distributed under the terms of the BSD license.
See README for more details.
Using interface 'wlan0'
Interactive mode
>
// 3. list 当前的网络,方便我们找到"TPLINK-TTLS"的network_id
> list_networks
network id / ssid / bssid / flags
0 TPLINK-TTLS any [TEMP-DISABLED]
>
// 4. 将EAP方法设定为"PEAP TTLS PWD"
> set_network 0 eap PEAP TTLS PWD
OK
>
// 5. 重新主动连接该网络
> select_network 0
OK
// 6. 结论:可以正常连接上EAP方法为TTLS的网络
5. Legacy Nak
上面的实验已经完全证明Android和IOS一样也支持设定多种EAP方法,根据认证服务器回应的认证挑战,回复多个EAP方法提供给认证服务器进行选择。在Wi-Fi EAP网络验证过程与Android平台拓展实例(一)中,不知有没有注意到"Legacy Nak"
,这是在手机(STA)发给AP(或者是认证服务器)的报文中:
/** Android*/
Extensible Authentication Protocol
Code: Response (2)
Id: 1
Length: 6 /** 注意这里的长度是6*/
Type: Legacy Nak (Response Only) (3)
Desired Auth Type: Protected EAP (EAP-PEAP) (25)
这份报文内容也是由wpa_supplicant进行组织的,其中说明wpa_supplican将会把所有设定的多个eap method封装成802.11消息发送给AP,下面我们简单来看一下:
/** external\wpa_supplicant_8\src\eap_peer\eap.c*/
static struct wpabuf * eap_sm_buildNak(struct eap_sm *sm, int id)
{
struct wpabuf *resp;
u8 *start;
int found = 0, expanded_found = 0;
// count是对应network中配置的eap method的个数
size_t count;
const struct eap_method *methods, *m;
wpa_printf(MSG_DEBUG, "EAP: Building EAP-Nak (requested type %u "
"vendor=%u method=%u not allowed)", sm->reqMethod,
sm->reqVendor, sm->reqVendorMethod);
// 循环获取eap_methods
methods = eap_peer_get_methods(&count);
if (methods == NULL)
return NULL;
if (sm->reqMethod == EAP_TYPE_EXPANDED)
return eap_sm_build_expanded_nak(sm, id, methods, count);
/* RFC 3748 - 5.3.1: Legacy Nak */
// 这里可以参考上份报文,对于"SET_NETWORK 0 eap PEAP",这里count=1,
// eap_msg_alloc alloc出6个字节的response
resp = eap_msg_alloc(EAP_VENDOR_IETF, EAP_TYPE_NAK,
sizeof(struct eap_hdr) + 1 + count + 1,
EAP_CODE_RESPONSE, id);
if (resp == NULL)
return NULL;
start = wpabuf_put(resp, 0);
// 校验config中的eap methods,并将其填充到resp中
for (m = methods; m; m = m->next) {
if (m->vendor == EAP_VENDOR_IETF && m->method == sm->reqMethod)
continue; /* do not allow the current method again */
if (eap_allowed_method(sm, m->vendor, m->method)) {
if (m->vendor != EAP_VENDOR_IETF) {
if (expanded_found)
continue;
expanded_found = 1;
wpabuf_put_u8(resp, EAP_TYPE_EXPANDED);
} else
// 将method这一个字节填入道resp中
wpabuf_put_u8(resp, m->method);
found++;
}
}
...
return resp;
5. 总结
经过Wi-Fi EAP网络验证过程与Android平台拓展实例(一)和本文的分析,我们已经大致了解了EAP网络的认证流程以及Android系统是如何处理EAP方法的,我们可以知道,造成Android和IOS在认证上的不同点是因为Android framework没有支持多种eap method,但是wpa_supplicant中已经支持。因此,不用修改Android中的eap验证协议,仅仅需要稍加修改framework和Settings的部分逻辑,就可以实现类似IOS那种简洁的登录方式。