| /* |
| * 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; |
| } |
| } |