blob: 225d01c7e55d10b3226fd112e6c4d6ae12f34ca7 [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiContext;
import android.net.wifi.WifiEnterpriseConfig;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.server.wifi.util.CertificateSubjectInfo;
import com.android.server.wifi.util.NativeUtil;
import com.android.wifi.resources.R;
import java.security.cert.X509Certificate;
/** This class is used to handle insecure EAP networks. */
public class InsecureEapNetworkHandler {
private static final String TAG = "InsecureEapNetworkHandler";
@VisibleForTesting
static final String ACTION_CERT_NOTIF_TAP =
"com.android.server.wifi.ClientModeImpl.ACTION_CERT_NOTIF_TAP";
@VisibleForTesting
static final String ACTION_CERT_NOTIF_ACCEPT =
"com.android.server.wifi.ClientModeImpl.ACTION_CERT_NOTIF_ACCEPT";
@VisibleForTesting
static final String ACTION_CERT_NOTIF_REJECT =
"com.android.server.wifi.ClientModeImpl.ACTION_CERT_NOTIF_REJECT";
@VisibleForTesting
static final String EXTRA_PENDING_CERT_SSID =
"com.android.server.wifi.ClientModeImpl.EXTRA_PENDING_CERT_SSID";
private final String mCaCertHelpLink;
private final WifiContext mContext;
private final WifiConfigManager mWifiConfigManager;
private final WifiNative mWifiNative;
private final FrameworkFacade mFacade;
private final WifiNotificationManager mNotificationManager;
private final WifiDialogManager mWifiDialogManager;
private final boolean mIsTrustOnFirstUseSupported;
private final boolean mIsInsecureEnterpriseConfigurationAllowed;
private final InsecureEapNetworkHandlerCallbacks mCallbacks;
private final String mInterfaceName;
private final Handler mHandler;
@NonNull
private WifiConfiguration mCurConfig = null;
private int mPendingCaCertDepth = -1;
@Nullable
private X509Certificate mPendingCaCert = null;
@Nullable
private X509Certificate mPendingServerCert = null;
// This is updated on setting a pending CA cert.
private CertificateSubjectInfo mPendingCaCertSubjectInfo = null;
// This is updated on setting a pending CA cert.
private CertificateSubjectInfo mPendingCaCertIssuerInfo = null;
@Nullable
private WifiDialogManager.DialogHandle mTofuAlertDialog = null;
private boolean mIsCertNotificationReceiverRegistered = false;
BroadcastReceiver mCertNotificationReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
String ssid = intent.getStringExtra(EXTRA_PENDING_CERT_SSID);
// This is an onGoing notification, dismiss it once an action is sent.
dismissDialogAndNotification();
Log.d(TAG, "Received CertNotification: ssid=" + ssid + ", action=" + action);
if (TextUtils.equals(action, ACTION_CERT_NOTIF_TAP)) {
askForUserApprovalForCaCertificate();
} else if (TextUtils.equals(action, ACTION_CERT_NOTIF_ACCEPT)) {
handleAccept(ssid);
} else if (TextUtils.equals(action, ACTION_CERT_NOTIF_REJECT)) {
handleReject(ssid);
}
}
};
public InsecureEapNetworkHandler(@NonNull WifiContext context,
@NonNull WifiConfigManager wifiConfigManager,
@NonNull WifiNative wifiNative,
@NonNull FrameworkFacade facade,
@NonNull WifiNotificationManager notificationManager,
@NonNull WifiDialogManager wifiDialogManager,
boolean isTrustOnFirstUseSupported,
boolean isInsecureEnterpriseConfigurationAllowed,
@NonNull InsecureEapNetworkHandlerCallbacks callbacks,
@NonNull String interfaceName,
@NonNull Handler handler) {
mContext = context;
mWifiConfigManager = wifiConfigManager;
mWifiNative = wifiNative;
mFacade = facade;
mNotificationManager = notificationManager;
mWifiDialogManager = wifiDialogManager;
mIsTrustOnFirstUseSupported = isTrustOnFirstUseSupported;
mIsInsecureEnterpriseConfigurationAllowed = isInsecureEnterpriseConfigurationAllowed;
mCallbacks = callbacks;
mInterfaceName = interfaceName;
mHandler = handler;
mCaCertHelpLink = mContext.getString(R.string.config_wifiCertInstallationHelpLink);
}
/**
* Prepare data for a new connection.
*
* Prepare data if this is an Enterprise configuration, which
* uses Server Cert, without a valid Root CA certificate or user approval.
*
* @param config the running wifi configuration.
*/
public void prepareConnection(@NonNull WifiConfiguration config) {
if (null == config) return;
if (!config.isEnterprise()) return;
WifiEnterpriseConfig entConfig = config.enterpriseConfig;
if (!entConfig.isEapMethodServerCertUsed()) return;
if (entConfig.hasCaCertificate()) return;
clearConnection();
Log.d(TAG, "prepareConnection: isTofuSupported=" + mIsTrustOnFirstUseSupported
+ ", isInsecureEapNetworkAllowed=" + mIsInsecureEnterpriseConfigurationAllowed
+ ", isTofuEnabled=" + entConfig.isTrustOnFirstUseEnabled()
+ ", isUserApprovedNoCaCert=" + entConfig.isUserApproveNoCaCert());
// If TOFU is not supported or insecure EAP network is allowed without TOFU enabled,
// return to skip the dialog if this network is approved before.
if (entConfig.isUserApproveNoCaCert()) {
if (!mIsTrustOnFirstUseSupported) return;
if (mIsInsecureEnterpriseConfigurationAllowed
&& !entConfig.isTrustOnFirstUseEnabled()) {
return;
}
}
mCurConfig = config;
registerCertificateNotificationReceiver();
// Remove cached PMK in the framework and supplicant to avoid
// skipping the EAP flow.
clearNativeData();
Log.d(TAG, "Remove native cached data and networks for TOFU.");
}
/** Clear data on disconnecting a connection. */
private void clearConnection() {
unregisterCertificateNotificationReceiver();
dismissDialogAndNotification();
clearInternalData();
}
/**
* Store the received certifiate for later use.
*
* @param ssid the target network SSID.
* @param depth the depth of this cert. The Root CA should be 0 or
* a positive number, and the server cert is 0.
* @param cert the Root CA certificate from the server.
* @return true if the cert is cached; otherwise, false.
*/
public boolean setPendingCertificate(@NonNull String ssid, int depth,
@NonNull X509Certificate cert) {
Log.d(TAG, "setPendingCertificate: " + "ssid=" + ssid + " depth=" + depth
+ " current config=" + mCurConfig);
if (TextUtils.isEmpty(ssid)) return false;
if (null == mCurConfig) return false;
if (!TextUtils.equals(ssid, mCurConfig.SSID)) return false;
if (null == cert) return false;
if (depth < 0) return false;
// 0 is the tail, i.e. the server cert.
if (depth == 0 && null == mPendingServerCert) {
mPendingServerCert = cert;
Log.d(TAG, "Pending server certificate: " + mPendingServerCert);
}
if (depth < mPendingCaCertDepth) {
Log.d(TAG, "Ignore intermediate cert." + cert);
return true;
}
mPendingCaCertSubjectInfo = CertificateSubjectInfo.parse(
cert.getSubjectDN().getName());
if (null == mPendingCaCertSubjectInfo) {
Log.e(TAG, "CA cert has no valid subject.");
return false;
}
mPendingCaCertIssuerInfo = CertificateSubjectInfo.parse(
cert.getIssuerDN().getName());
if (null == mPendingCaCertIssuerInfo) {
Log.e(TAG, "CA cert has no valid issuer.");
return false;
}
mPendingCaCertDepth = depth;
mPendingCaCert = cert;
Log.d(TAG, "Pending Root CA certificate: " + mPendingCaCert);
return true;
}
/**
* Ask for the user approval if necessary.
*
* For TOFU is supported and an EAP network without a CA certificate.
* - if insecure EAP networks are not allowed
* - if TOFU is not enabled, disconnect it.
* - if no pending CA cert, disconnect it.
* - if no server cert, disconnect it.
* - if insecure EAP networks are allowed and TOFU is not enabled
* - follow no TOFU support flow.
* - if TOFU is enabled, CA cert is pending, and server cert is pending
* - gate the connecitvity event here
* - if this request is from a user, launch a dialog to get the user approval.
* - if this request is from auto-connect, launch a notification.
* If TOFU is not supported, the confirmation flow is similar. Instead of installing CA
* cert from the server, just mark this network is approved by the user.
*
* @param isUserSelected indicates that this connection is triggered by a user.
* @return true if the user approval is needed; otherwise, false.
*/
public boolean startUserApprovalIfNecessary(boolean isUserSelected) {
if (null == mCurConfig) return false;
if (!mCurConfig.isEnterprise()) return false;
WifiEnterpriseConfig entConfig = mCurConfig.enterpriseConfig;
if (!entConfig.isEapMethodServerCertUsed()) return false;
if (entConfig.hasCaCertificate()) return false;
// If Trust On First Use is supported and insecure enterprise configuration
// is not allowed, TOFU must be used for an Enterprise network without certs.
if (mIsTrustOnFirstUseSupported && !mIsInsecureEnterpriseConfigurationAllowed
&& !mCurConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
Log.d(TAG, "Trust On First Use is not enabled.");
handleError(mCurConfig.SSID);
return true;
}
if (useTrustOnFirstUse()) {
if (null == mPendingCaCert) {
Log.d(TAG, "No valid CA cert for TLS-based connection.");
handleError(mCurConfig.SSID);
return true;
} else if (null == mPendingServerCert) {
Log.d(TAG, "No valid Server cert for TLS-based connection.");
handleError(mCurConfig.SSID);
return true;
}
}
Log.d(TAG, "startUserApprovalIfNecessaryForInsecureEapNetwork: mIsUserSelected="
+ isUserSelected);
if (isUserSelected) {
askForUserApprovalForCaCertificate();
} else {
notifyUserForCaCertificate();
}
return true;
}
private boolean useTrustOnFirstUse() {
return mIsTrustOnFirstUseSupported
&& mCurConfig.enterpriseConfig.isTrustOnFirstUseEnabled();
}
private void registerCertificateNotificationReceiver() {
if (mIsCertNotificationReceiverRegistered) return;
IntentFilter filter = new IntentFilter();
if (useTrustOnFirstUse()) {
filter.addAction(ACTION_CERT_NOTIF_TAP);
} else {
filter.addAction(ACTION_CERT_NOTIF_ACCEPT);
filter.addAction(ACTION_CERT_NOTIF_REJECT);
}
mContext.registerReceiver(mCertNotificationReceiver, filter, null, mHandler);
mIsCertNotificationReceiverRegistered = true;
}
private void unregisterCertificateNotificationReceiver() {
if (!mIsCertNotificationReceiverRegistered) return;
mContext.unregisterReceiver(mCertNotificationReceiver);
mIsCertNotificationReceiverRegistered = false;
}
@VisibleForTesting
void handleAccept(@NonNull String ssid) {
if (!isConnectionValid(ssid)) return;
if (!useTrustOnFirstUse()) {
mWifiConfigManager.setUserApproveNoCaCert(mCurConfig.networkId, true);
} else {
if (null == mPendingCaCert || null == mPendingServerCert) {
handleError(ssid);
return;
}
if (!mWifiConfigManager.updateCaCertificate(
mCurConfig.networkId, mPendingCaCert, mPendingServerCert)) {
// The user approved this network,
// keep the connection regardless of the result.
Log.e(TAG, "Cannot update CA cert to network " + mCurConfig.getProfileKey()
+ ", CA cert = " + mPendingCaCert);
}
}
mWifiConfigManager.allowAutojoin(mCurConfig.networkId, true);
dismissDialogAndNotification();
clearInternalData();
if (null != mCallbacks) mCallbacks.onAccept(ssid);
}
@VisibleForTesting
void handleReject(@NonNull String ssid) {
if (!isConnectionValid(ssid)) return;
mWifiConfigManager.allowAutojoin(mCurConfig.networkId, false);
dismissDialogAndNotification();
clearInternalData();
clearNativeData();
if (null != mCallbacks) mCallbacks.onReject(ssid);
}
private void handleError(@Nullable String ssid) {
dismissDialogAndNotification();
clearInternalData();
clearNativeData();
if (null != mCallbacks) mCallbacks.onError(ssid);
}
private void askForUserApprovalForCaCertificate() {
if (mCurConfig == null || TextUtils.isEmpty(mCurConfig.SSID)) return;
if (useTrustOnFirstUse()) {
if (null == mPendingCaCert || null == mPendingServerCert) {
Log.e(TAG, "Cannot launch a dialog for TOFU without "
+ "a valid pending CA certificate.");
return;
}
}
dismissDialogAndNotification();
String title = useTrustOnFirstUse()
? mContext.getString(R.string.wifi_ca_cert_dialog_title)
: mContext.getString(R.string.wifi_ca_cert_dialog_preT_title);
String positiveButtonText = useTrustOnFirstUse()
? mContext.getString(R.string.wifi_ca_cert_dialog_continue_text)
: mContext.getString(R.string.wifi_ca_cert_dialog_preT_continue_text);
String negativeButtonText = useTrustOnFirstUse()
? mContext.getString(R.string.wifi_ca_cert_dialog_abort_text)
: mContext.getString(R.string.wifi_ca_cert_dialog_preT_abort_text);
String message = null;
String messageUrl = null;
int messageUrlStart = 0;
int messageUrlEnd = 0;
if (useTrustOnFirstUse()) {
String signature = NativeUtil.hexStringFromByteArray(
mPendingCaCert.getSignature());
StringBuilder contentBuilder = new StringBuilder()
.append(mContext.getString(R.string.wifi_ca_cert_dialog_message_hint))
.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_server_name_text,
mPendingCaCertSubjectInfo.commonName))
.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_issuer_name_text,
mPendingCaCertIssuerInfo.commonName));
if (!TextUtils.isEmpty(mPendingCaCertSubjectInfo.organization)) {
contentBuilder.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_organization_text,
mPendingCaCertSubjectInfo.organization));
}
if (!TextUtils.isEmpty(mPendingCaCertSubjectInfo.email)) {
contentBuilder.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_contact_text,
mPendingCaCertSubjectInfo.email));
}
contentBuilder
.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_signature_name_text,
signature.substring(0, 16)));
message = contentBuilder.toString();
} else {
String hint = mContext.getString(
R.string.wifi_ca_cert_dialog_preT_message_hint, mCurConfig.SSID);
String linkText = mContext.getString(
R.string.wifi_ca_cert_dialog_preT_message_link);
message = hint + " " + linkText;
messageUrl = mCaCertHelpLink;
messageUrlStart = hint.length() + 1;
messageUrlEnd = message.length();
}
mTofuAlertDialog = mWifiDialogManager.createSimpleDialogWithUrl(
title,
message,
messageUrl,
messageUrlStart,
messageUrlEnd,
positiveButtonText,
negativeButtonText,
null /* neutralButtonText */,
new WifiDialogManager.SimpleDialogCallback() {
@Override
public void onPositiveButtonClicked() {
handleAccept(mCurConfig.SSID);
}
@Override
public void onNegativeButtonClicked() {
handleReject(mCurConfig.SSID);
}
@Override
public void onNeutralButtonClicked() {
// Not used.
handleReject(mCurConfig.SSID);
}
@Override
public void onCancelled() {
handleReject(mCurConfig.SSID);
}
},
new WifiThreadRunner(mHandler));
mTofuAlertDialog.launchDialog();
}
private PendingIntent genCaCertNotifIntent(
@NonNull String action, @NonNull String ssid) {
Intent intent = new Intent(action)
.setPackage(mContext.getServiceWifiPackageName())
.putExtra(EXTRA_PENDING_CERT_SSID, ssid);
return mFacade.getBroadcast(mContext, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
private void notifyUserForCaCertificate() {
if (mCurConfig == null) return;
if (useTrustOnFirstUse()) {
if (null == mPendingCaCert) return;
if (null == mPendingServerCert) return;
}
dismissDialogAndNotification();
PendingIntent tapPendingIntent;
if (useTrustOnFirstUse()) {
tapPendingIntent = genCaCertNotifIntent(ACTION_CERT_NOTIF_TAP, mCurConfig.SSID);
} else {
Intent openLinkIntent = new Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(mCaCertHelpLink))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
tapPendingIntent = mFacade.getActivity(mContext, 0, openLinkIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
String title = useTrustOnFirstUse()
? mContext.getString(R.string.wifi_ca_cert_notification_title)
: mContext.getString(R.string.wifi_ca_cert_notification_preT_title);
String content = useTrustOnFirstUse()
? mContext.getString(R.string.wifi_ca_cert_notification_message, mCurConfig.SSID)
: mContext.getString(R.string.wifi_ca_cert_notification_preT_message,
mCurConfig.SSID);
Notification.Builder builder = mFacade.makeNotificationBuilder(mContext,
WifiService.NOTIFICATION_NETWORK_ALERTS)
.setSmallIcon(Icon.createWithResource(mContext.getWifiOverlayApkPkgName(),
com.android.wifi.resources.R.drawable.stat_notify_wifi_in_range))
.setContentTitle(title)
.setContentText(content)
.setStyle(new Notification.BigTextStyle().bigText(content))
.setContentIntent(tapPendingIntent)
.setOngoing(true)
.setColor(mContext.getResources().getColor(
android.R.color.system_notification_accent_color));
// On a device which does not support Trust On First Use,
// a user can accept or reject this network via the notification.
if (!useTrustOnFirstUse()) {
Notification.Action acceptAction = new Notification.Action.Builder(
null /* icon */,
mContext.getString(R.string.wifi_ca_cert_dialog_preT_continue_text),
genCaCertNotifIntent(ACTION_CERT_NOTIF_ACCEPT, mCurConfig.SSID)).build();
Notification.Action rejectAction = new Notification.Action.Builder(
null /* icon */,
mContext.getString(R.string.wifi_ca_cert_dialog_preT_abort_text),
genCaCertNotifIntent(ACTION_CERT_NOTIF_REJECT, mCurConfig.SSID)).build();
builder.addAction(rejectAction).addAction(acceptAction);
}
mNotificationManager.notify(SystemMessage.NOTE_SERVER_CA_CERTIFICATE, builder.build());
}
private void dismissDialogAndNotification() {
mNotificationManager.cancel(SystemMessage.NOTE_SERVER_CA_CERTIFICATE);
if (mTofuAlertDialog != null) {
mTofuAlertDialog.dismissDialog();
mTofuAlertDialog = null;
}
}
private void clearInternalData() {
mPendingCaCertDepth = -1;
mPendingCaCert = null;
mPendingServerCert = null;
mPendingCaCertSubjectInfo = null;
mPendingCaCertIssuerInfo = null;
mCurConfig = null;
}
private void clearNativeData() {
// PMK should be cleared or it would skip EAP flow next time.
if (null != mCurConfig) {
mWifiNative.removeNetworkCachedData(mCurConfig.networkId);
}
// remove network so that supplicant's PMKSA cache is cleared
mWifiNative.removeAllNetworks(mInterfaceName);
}
// There might be two possible conditions that there is no
// valid information to handle this response:
// 1. A new network request is fired just before getting the response.
// As a result, this response is invalid and should be ignored.
// 2. There is something wrong, and it stops at an abnormal state.
// For this case, we should go back DisconnectedState to
// recover the state machine.
// Unfortunatually, we cannot identify the condition without valid information.
// If condition #1 occurs, and we found that the target SSID is changed,
// it should transit to L3Connected soon normally, just ignore this message.
// If condition #2 occurs, clear existing data and notify the client mode
// via onError callback.
private boolean isConnectionValid(@Nullable String ssid) {
if (TextUtils.isEmpty(ssid) || null == mCurConfig) {
handleError(null);
return false;
}
if (!TextUtils.equals(ssid, mCurConfig.SSID)) {
Log.w(TAG, "Target SSID " + mCurConfig.SSID
+ " is different from TOFU returned SSID" + ssid);
return false;
}
return true;
}
/** The callbacks object to notify the consumer. */
public static class InsecureEapNetworkHandlerCallbacks {
/**
* When a certificate is accepted, this callback is called.
*
* @param ssid SSID of the network.
*/
public void onAccept(@NonNull String ssid) {}
/**
* When a certificate is rejected, this callback is called.
*
* @param ssid SSID of the network.
*/
public void onReject(@NonNull String ssid) {}
/**
* When there are no valid data to handle this insecure EAP network,
* this callback is called.
*
* @param ssid SSID of the network, it might be null.
*/
public void onError(@Nullable String ssid) {}
}
}