/*
|
* Copyright (C) 2018 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;
|
|
import static android.net.wifi.WifiManager.EASY_CONNECT_NETWORK_ROLE_AP;
|
|
import android.content.Context;
|
import android.hardware.wifi.supplicant.V1_2.DppAkm;
|
import android.hardware.wifi.supplicant.V1_2.DppFailureCode;
|
import android.hardware.wifi.supplicant.V1_2.DppNetRole;
|
import android.hardware.wifi.supplicant.V1_2.DppProgressCode;
|
import android.net.wifi.EasyConnectStatusCallback;
|
import android.net.wifi.IDppCallback;
|
import android.net.wifi.WifiConfiguration;
|
import android.net.wifi.WifiManager;
|
import android.os.Handler;
|
import android.os.IBinder;
|
import android.os.Looper;
|
import android.os.RemoteException;
|
import android.util.Log;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.util.WakeupMessage;
|
import com.android.server.wifi.WifiNative.DppEventCallback;
|
|
/**
|
* DPP Manager class
|
* Implements the DPP Initiator APIs and callbacks
|
*/
|
public class DppManager {
|
private static final String TAG = "DppManager";
|
public Handler mHandler;
|
|
private DppRequestInfo mDppRequestInfo = null;
|
private final WifiNative mWifiNative;
|
private String mClientIfaceName;
|
private boolean mVerboseLoggingEnabled;
|
WifiConfigManager mWifiConfigManager;
|
private final Context mContext;
|
@VisibleForTesting
|
public WakeupMessage mDppTimeoutMessage = null;
|
private final Clock mClock;
|
private static final String DPP_TIMEOUT_TAG = TAG + " Request Timeout";
|
private static final int DPP_TIMEOUT_MS = 40_000; // 40 seconds
|
private final DppMetrics mDppMetrics;
|
|
private final DppEventCallback mDppEventCallback = new DppEventCallback() {
|
@Override
|
public void onSuccessConfigReceived(WifiConfiguration newWifiConfiguration) {
|
mHandler.post(() -> {
|
DppManager.this.onSuccessConfigReceived(newWifiConfiguration);
|
});
|
}
|
|
@Override
|
public void onSuccessConfigSent() {
|
mHandler.post(() -> {
|
DppManager.this.onSuccessConfigSent();
|
});
|
}
|
|
@Override
|
public void onProgress(int dppStatusCode) {
|
mHandler.post(() -> {
|
DppManager.this.onProgress(dppStatusCode);
|
});
|
}
|
|
@Override
|
public void onFailure(int dppStatusCode) {
|
mHandler.post(() -> {
|
DppManager.this.onFailure(dppStatusCode);
|
});
|
}
|
};
|
|
DppManager(Looper looper, WifiNative wifiNative, WifiConfigManager wifiConfigManager,
|
Context context, DppMetrics dppMetrics) {
|
mHandler = new Handler(looper);
|
mWifiNative = wifiNative;
|
mWifiConfigManager = wifiConfigManager;
|
mWifiNative.registerDppEventCallback(mDppEventCallback);
|
mContext = context;
|
mClock = new Clock();
|
mDppMetrics = dppMetrics;
|
|
// Setup timer
|
mDppTimeoutMessage = new WakeupMessage(mContext, mHandler,
|
DPP_TIMEOUT_TAG, () -> {
|
timeoutDppRequest();
|
});
|
}
|
|
private static String encodeStringToHex(String str) {
|
if ((str.length() > 1) && (str.charAt(0) == '"') && (str.charAt(str.length() - 1) == '"')) {
|
// Remove the surrounding quotes
|
str = str.substring(1, str.length() - 1);
|
|
// Convert to Hex
|
char[] charsArray = str.toCharArray();
|
StringBuffer hexBuffer = new StringBuffer();
|
for (int i = 0; i < charsArray.length; i++) {
|
hexBuffer.append(Integer.toHexString((int) charsArray[i]));
|
}
|
return hexBuffer.toString();
|
}
|
return str;
|
}
|
|
private void timeoutDppRequest() {
|
logd("DPP timeout");
|
|
if (mDppRequestInfo == null) {
|
Log.e(TAG, "DPP timeout with no request info");
|
return;
|
}
|
|
// Clean up supplicant resources
|
if (!mWifiNative.stopDppInitiator(mClientIfaceName)) {
|
Log.e(TAG, "Failed to stop DPP Initiator");
|
}
|
|
// Clean up resources and let the caller know about the timeout
|
onFailure(DppFailureCode.TIMEOUT);
|
}
|
|
/**
|
* Start DPP request in Configurator-Initiator mode. The goal of this call is to send the
|
* selected Wi-Fi configuration to a remote peer so it could join that network.
|
*
|
* @param uid User ID
|
* @param binder Binder object
|
* @param enrolleeUri The Enrollee URI, scanned externally (e.g. via QR code)
|
* @param selectedNetworkId The selected Wi-Fi network ID to be sent
|
* @param enrolleeNetworkRole Network role of remote enrollee: STA or AP
|
* @param callback DPP Callback object
|
*/
|
public void startDppAsConfiguratorInitiator(int uid, IBinder binder,
|
String enrolleeUri, int selectedNetworkId,
|
@WifiManager.EasyConnectNetworkRole int enrolleeNetworkRole, IDppCallback callback) {
|
mDppMetrics.updateDppConfiguratorInitiatorRequests();
|
if (mDppRequestInfo != null) {
|
try {
|
Log.e(TAG, "DPP request already in progress");
|
Log.e(TAG, "Ongoing request UID: " + mDppRequestInfo.uid + ", new UID: "
|
+ uid);
|
|
mDppMetrics.updateDppFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_BUSY);
|
// On going DPP. Call the failure callback directly
|
callback.onFailure(EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_BUSY);
|
} catch (RemoteException e) {
|
// Empty
|
}
|
return;
|
}
|
|
mClientIfaceName = mWifiNative.getClientInterfaceName();
|
if (mClientIfaceName == null) {
|
try {
|
Log.e(TAG, "Wi-Fi client interface does not exist");
|
// On going DPP. Call the failure callback directly
|
mDppMetrics.updateDppFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_GENERIC);
|
callback.onFailure(EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC);
|
} catch (RemoteException e) {
|
// Empty
|
}
|
return;
|
}
|
|
WifiConfiguration selectedNetwork = mWifiConfigManager
|
.getConfiguredNetworkWithoutMasking(selectedNetworkId);
|
|
if (selectedNetwork == null) {
|
try {
|
Log.e(TAG, "Selected network is null");
|
// On going DPP. Call the failure callback directly
|
mDppMetrics.updateDppFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK);
|
callback.onFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK);
|
} catch (RemoteException e) {
|
// Empty
|
}
|
return;
|
}
|
|
String password = null;
|
String psk = null;
|
int securityAkm;
|
|
// Currently support either SAE mode or PSK mode
|
if (selectedNetwork.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.SAE)) {
|
// SAE
|
password = selectedNetwork.preSharedKey;
|
securityAkm = DppAkm.SAE;
|
} else if (selectedNetwork.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK)) {
|
if (selectedNetwork.preSharedKey.matches(String.format("[0-9A-Fa-f]{%d}", 64))) {
|
// PSK
|
psk = selectedNetwork.preSharedKey;
|
} else {
|
// Passphrase
|
password = selectedNetwork.preSharedKey;
|
}
|
securityAkm = DppAkm.PSK;
|
} else {
|
try {
|
// Key management must be either PSK or SAE
|
Log.e(TAG, "Key management must be either PSK or SAE");
|
mDppMetrics.updateDppFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK);
|
callback.onFailure(
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK);
|
} catch (RemoteException e) {
|
// Empty
|
}
|
return;
|
}
|
|
mDppRequestInfo = new DppRequestInfo();
|
mDppRequestInfo.uid = uid;
|
mDppRequestInfo.binder = binder;
|
mDppRequestInfo.callback = callback;
|
|
if (!linkToDeath(mDppRequestInfo)) {
|
// Notify failure and clean up
|
onFailure(EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC);
|
return;
|
}
|
|
logd("Interface " + mClientIfaceName + ": Initializing URI: " + enrolleeUri);
|
|
mDppRequestInfo.startTime = mClock.getElapsedSinceBootMillis();
|
mDppTimeoutMessage.schedule(mDppRequestInfo.startTime + DPP_TIMEOUT_MS);
|
|
// Send Enrollee URI and get a peer ID
|
int peerId = mWifiNative.addDppPeerUri(mClientIfaceName, enrolleeUri);
|
|
if (peerId < 0) {
|
Log.e(TAG, "DPP add URI failure");
|
|
// Notify failure and clean up
|
onFailure(DppFailureCode.INVALID_URI);
|
return;
|
}
|
mDppRequestInfo.peerId = peerId;
|
|
// Auth init
|
logd("Authenticating");
|
|
String ssidEncoded = encodeStringToHex(selectedNetwork.SSID);
|
String passwordEncoded = null;
|
|
if (password != null) {
|
passwordEncoded = encodeStringToHex(selectedNetwork.preSharedKey);
|
}
|
|
if (!mWifiNative.startDppConfiguratorInitiator(mClientIfaceName,
|
mDppRequestInfo.peerId, 0, ssidEncoded, passwordEncoded, psk,
|
enrolleeNetworkRole == EASY_CONNECT_NETWORK_ROLE_AP ? DppNetRole.AP
|
: DppNetRole.STA,
|
securityAkm)) {
|
Log.e(TAG, "DPP Start Configurator Initiator failure");
|
|
// Notify failure and clean up
|
onFailure(DppFailureCode.FAILURE);
|
return;
|
}
|
|
logd("Success: Started DPP Initiator with peer ID "
|
+ mDppRequestInfo.peerId);
|
}
|
|
/**
|
* Start DPP request in Enrollee-Initiator mode. The goal of this call is to receive a
|
* Wi-Fi configuration object from the peer configurator in order to join a network.
|
*
|
* @param uid User ID
|
* @param binder Binder object
|
* @param configuratorUri The Configurator URI, scanned externally (e.g. via QR code)
|
* @param callback DPP Callback object
|
*/
|
public void startDppAsEnrolleeInitiator(int uid, IBinder binder,
|
String configuratorUri, IDppCallback callback) {
|
mDppMetrics.updateDppEnrolleeInitiatorRequests();
|
if (mDppRequestInfo != null) {
|
try {
|
Log.e(TAG, "DPP request already in progress");
|
Log.e(TAG, "Ongoing request UID: " + mDppRequestInfo.uid + ", new UID: "
|
+ uid);
|
|
mDppMetrics.updateDppFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_BUSY);
|
// On going DPP. Call the failure callback directly
|
callback.onFailure(EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_BUSY);
|
} catch (RemoteException e) {
|
// Empty
|
}
|
return;
|
}
|
|
mDppRequestInfo = new DppRequestInfo();
|
mDppRequestInfo.uid = uid;
|
mDppRequestInfo.binder = binder;
|
mDppRequestInfo.callback = callback;
|
|
if (!linkToDeath(mDppRequestInfo)) {
|
// Notify failure and clean up
|
onFailure(DppFailureCode.FAILURE);
|
return;
|
}
|
|
mDppRequestInfo.startTime = mClock.getElapsedSinceBootMillis();
|
mDppTimeoutMessage.schedule(mDppRequestInfo.startTime + DPP_TIMEOUT_MS);
|
|
mClientIfaceName = mWifiNative.getClientInterfaceName();
|
logd("Interface " + mClientIfaceName + ": Initializing URI: " + configuratorUri);
|
|
// Send Configurator URI and get a peer ID
|
int peerId = mWifiNative.addDppPeerUri(mClientIfaceName, configuratorUri);
|
|
if (peerId < 0) {
|
Log.e(TAG, "DPP add URI failure");
|
onFailure(DppFailureCode.INVALID_URI);
|
return;
|
}
|
mDppRequestInfo.peerId = peerId;
|
|
// Auth init
|
logd("Authenticating");
|
|
if (!mWifiNative.startDppEnrolleeInitiator(mClientIfaceName, mDppRequestInfo.peerId,
|
0)) {
|
Log.e(TAG, "DPP Start Enrollee Initiator failure");
|
|
// Notify failure and clean up
|
onFailure(DppFailureCode.FAILURE);
|
return;
|
}
|
|
logd("Success: Started DPP Initiator with peer ID "
|
+ mDppRequestInfo.peerId);
|
}
|
|
/**
|
* Stop a current DPP session
|
*
|
* @param uid User ID
|
*/
|
public void stopDppSession(int uid) {
|
if (mDppRequestInfo == null) {
|
logd("UID " + uid + " called stop DPP session with no active DPP session");
|
return;
|
}
|
|
if (mDppRequestInfo.uid != uid) {
|
Log.e(TAG, "UID " + uid + " called stop DPP session but UID " + mDppRequestInfo.uid
|
+ " has started it");
|
return;
|
}
|
|
// Clean up supplicant resources
|
if (!mWifiNative.stopDppInitiator(mClientIfaceName)) {
|
Log.e(TAG, "Failed to stop DPP Initiator");
|
}
|
|
cleanupDppResources();
|
|
logd("Success: Stopped DPP Initiator");
|
}
|
|
private void cleanupDppResources() {
|
logd("DPP clean up resources");
|
if (mDppRequestInfo == null) {
|
return;
|
}
|
|
// Cancel pending timeout
|
mDppTimeoutMessage.cancel();
|
|
// Remove the URI from the supplicant list
|
if (!mWifiNative.removeDppUri(mClientIfaceName, mDppRequestInfo.peerId)) {
|
Log.e(TAG, "Failed to remove DPP URI ID " + mDppRequestInfo.peerId);
|
}
|
|
mDppRequestInfo.binder.unlinkToDeath(mDppRequestInfo.dr, 0);
|
|
mDppRequestInfo = null;
|
}
|
|
private static class DppRequestInfo {
|
public int uid;
|
public IBinder binder;
|
public IBinder.DeathRecipient dr;
|
public int peerId;
|
public IDppCallback callback;
|
public long startTime;
|
|
@Override
|
public String toString() {
|
return new StringBuilder("DppRequestInfo: uid=").append(uid).append(", binder=").append(
|
binder).append(", dr=").append(dr)
|
.append(", callback=").append(callback)
|
.append(", peerId=").append(peerId).toString();
|
}
|
}
|
|
/**
|
* Enable vervose logging from DppManager
|
*
|
* @param verbose 0 to disable verbose logging, or any other value to enable.
|
*/
|
public void enableVerboseLogging(int verbose) {
|
mVerboseLoggingEnabled = verbose != 0 ? true : false;
|
}
|
|
private void onSuccessConfigReceived(WifiConfiguration newWifiConfiguration) {
|
try {
|
logd("onSuccessConfigReceived");
|
|
if (mDppRequestInfo != null) {
|
long now = mClock.getElapsedSinceBootMillis();
|
mDppMetrics.updateDppOperationTime((int) (now - mDppRequestInfo.startTime));
|
|
NetworkUpdateResult networkUpdateResult = mWifiConfigManager
|
.addOrUpdateNetwork(newWifiConfiguration, mDppRequestInfo.uid);
|
|
if (networkUpdateResult.isSuccess()) {
|
mDppMetrics.updateDppEnrolleeSuccess();
|
mDppRequestInfo.callback.onSuccessConfigReceived(
|
networkUpdateResult.getNetworkId());
|
} else {
|
Log.e(TAG, "DPP configuration received, but failed to update network");
|
mDppMetrics.updateDppFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_CONFIGURATION);
|
mDppRequestInfo.callback.onFailure(EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_FAILURE_CONFIGURATION);
|
}
|
} else {
|
Log.e(TAG, "Unexpected null Wi-Fi configuration object");
|
}
|
} catch (RemoteException e) {
|
Log.e(TAG, "Callback failure");
|
}
|
|
// Success, DPP is complete. Clear the DPP session automatically
|
cleanupDppResources();
|
}
|
|
private void onSuccessConfigSent() {
|
try {
|
if (mDppRequestInfo == null) {
|
Log.e(TAG, "onDppSuccessConfigSent event without a request information object");
|
return;
|
}
|
|
logd("onSuccessConfigSent");
|
|
long now = mClock.getElapsedSinceBootMillis();
|
mDppMetrics.updateDppOperationTime((int) (now - mDppRequestInfo.startTime));
|
|
mDppMetrics.updateDppConfiguratorSuccess(
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_SUCCESS_CONFIGURATION_SENT);
|
mDppRequestInfo.callback.onSuccess(
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_SUCCESS_CONFIGURATION_SENT);
|
|
} catch (RemoteException e) {
|
Log.e(TAG, "Callback failure");
|
}
|
|
// Success, DPP is complete. Clear the DPP session automatically
|
cleanupDppResources();
|
}
|
|
private void onProgress(int dppStatusCode) {
|
try {
|
if (mDppRequestInfo == null) {
|
Log.e(TAG, "onProgress event without a request information object");
|
return;
|
}
|
|
logd("onProgress: " + dppStatusCode);
|
|
int dppProgressCode;
|
|
// Convert from HAL codes to WifiManager/user codes
|
switch (dppStatusCode) {
|
case DppProgressCode.AUTHENTICATION_SUCCESS:
|
dppProgressCode = EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_PROGRESS_AUTHENTICATION_SUCCESS;
|
break;
|
|
case DppProgressCode.RESPONSE_PENDING:
|
dppProgressCode = EasyConnectStatusCallback
|
.EASY_CONNECT_EVENT_PROGRESS_RESPONSE_PENDING;
|
break;
|
|
default:
|
Log.e(TAG, "onProgress: unknown code " + dppStatusCode);
|
return;
|
}
|
|
mDppRequestInfo.callback.onProgress(dppProgressCode);
|
|
} catch (RemoteException e) {
|
Log.e(TAG, "Callback failure");
|
}
|
}
|
|
private void onFailure(int dppStatusCode) {
|
try {
|
if (mDppRequestInfo == null) {
|
Log.e(TAG, "onFailure event without a request information object");
|
return;
|
}
|
|
logd("OnFailure: " + dppStatusCode);
|
|
long now = mClock.getElapsedSinceBootMillis();
|
mDppMetrics.updateDppOperationTime((int) (now - mDppRequestInfo.startTime));
|
|
int dppFailureCode;
|
|
// Convert from HAL codes to WifiManager/user codes
|
switch (dppStatusCode) {
|
case DppFailureCode.INVALID_URI:
|
dppFailureCode =
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_URI;
|
break;
|
|
case DppFailureCode.AUTHENTICATION:
|
dppFailureCode =
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION;
|
break;
|
|
case DppFailureCode.NOT_COMPATIBLE:
|
dppFailureCode =
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE;
|
break;
|
|
case DppFailureCode.CONFIGURATION:
|
dppFailureCode =
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_CONFIGURATION;
|
break;
|
|
case DppFailureCode.BUSY:
|
dppFailureCode = EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_BUSY;
|
break;
|
|
case DppFailureCode.TIMEOUT:
|
dppFailureCode = EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_TIMEOUT;
|
break;
|
|
case DppFailureCode.NOT_SUPPORTED:
|
dppFailureCode =
|
EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED;
|
break;
|
|
case DppFailureCode.FAILURE:
|
default:
|
dppFailureCode = EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC;
|
break;
|
}
|
|
mDppMetrics.updateDppFailure(dppFailureCode);
|
mDppRequestInfo.callback.onFailure(dppFailureCode);
|
|
} catch (RemoteException e) {
|
Log.e(TAG, "Callback failure");
|
}
|
|
// All failures are fatal, clear the DPP session
|
cleanupDppResources();
|
}
|
|
private void logd(String message) {
|
if (mVerboseLoggingEnabled) {
|
Log.d(TAG, message);
|
}
|
}
|
|
private boolean linkToDeath(DppRequestInfo dppRequestInfo) {
|
// register for binder death
|
dppRequestInfo.dr = new IBinder.DeathRecipient() {
|
@Override
|
public void binderDied() {
|
if (dppRequestInfo == null) {
|
return;
|
}
|
|
logd("binderDied: uid=" + dppRequestInfo.uid);
|
|
mHandler.post(() -> {
|
cleanupDppResources();
|
});
|
}
|
};
|
|
try {
|
dppRequestInfo.binder.linkToDeath(dppRequestInfo.dr, 0);
|
} catch (RemoteException e) {
|
Log.e(TAG, "Error on linkToDeath - " + e);
|
dppRequestInfo.dr = null;
|
return false;
|
}
|
|
return true;
|
}
|
}
|