blob: 7f5ffe0566ae23e8adc56d7fbe7a52ecdcd0f36c [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.annotation.SuppressLint;
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.text.format.DateFormat;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.util.HexDump;
import com.android.server.wifi.util.CertificateSubjectInfo;
import com.android.wifi.resources.R;
import java.security.InvalidAlgorithmParameterException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.LinkedList;
import java.util.Set;
import java.util.StringJoiner;
/** 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";
static final String TOFU_ANONYMOUS_IDENTITY = "anonymous";
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;
// The latest connecting configuration from the caller, it is updated on calling
// prepareConnection() always. This is used to ensure that current TOFU config is aligned
// with the caller connecting config.
@NonNull
private WifiConfiguration mConnectingConfig = null;
// The connecting configuration which is a valid TOFU configuration, it is updated
// only when the connecting configuration is a valid TOFU configuration and used
// by later TOFU procedure.
@NonNull
private WifiConfiguration mCurrentTofuConfig = null;
private int mPendingRootCaCertDepth = -1;
@Nullable
private X509Certificate mPendingRootCaCert = null;
@Nullable
private X509Certificate mPendingServerCert = null;
// This is updated on setting a pending server cert.
private CertificateSubjectInfo mPendingServerCertSubjectInfo = null;
// This is updated on setting a pending server cert.
private CertificateSubjectInfo mPendingServerCertIssuerInfo = null;
// Record the whole server cert chain from Root CA to the server cert.
// The order of the certificates in the chain required by the validation method is in the
// reverse order to the order we receive them from the lower layers. Therefore, we are using a
// LinkedList data type here, so that we could add certificates to the head, rather than
// using an ArrayList and then having to reverse it.
// Using SuppressLint here to avoid linter errors related to LinkedList usage.
@SuppressLint("JdkObsolete")
private LinkedList<X509Certificate> mServerCertChain = new LinkedList<>();
private WifiDialogManager.DialogHandle mTofuAlertDialog = null;
private boolean mIsCertNotificationReceiverRegistered = false;
private String mServerCertHash = null;
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 TOFU data for a new connection.
*
* Prepare TOFU data if this is an Enterprise configuration, which
* uses Server Cert, without a valid Root CA certificate or user approval.
* If TOFU is supported and enabled, this method will also clear the user credentials in the
* initial connection to the server.
*
* @param config the running wifi configuration.
*/
public void prepareConnection(@NonNull WifiConfiguration config) {
if (null == config) return;
mConnectingConfig = config;
if (!config.isEnterprise()) return;
WifiEnterpriseConfig entConfig = config.enterpriseConfig;
if (!entConfig.isEapMethodServerCertUsed()) return;
if (entConfig.hasCaCertificate()) return;
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,
// skip the entire TOFU logic if this network was approved earlier by the user.
if (entConfig.isUserApproveNoCaCert()) {
if (!mIsTrustOnFirstUseSupported) return;
if (mIsInsecureEnterpriseConfigurationAllowed
&& !entConfig.isTrustOnFirstUseEnabled()) {
return;
}
}
if (mIsTrustOnFirstUseSupported && (entConfig.isTrustOnFirstUseEnabled()
|| !mIsInsecureEnterpriseConfigurationAllowed)) {
/**
* Clear the user credentials from this copy of the configuration object.
* Supplicant will start the phase-1 TLS session to acquire the server certificate chain
* which will be provided to the framework. Then since the callbacks for identity and
* password requests are not populated, it will fail the connection and disconnect.
* This will allow the user to review the certificates at their own pace, and a
* reconnection would automatically take place with full verification of the chain once
* they approve.
*/
if (config.enterpriseConfig.getEapMethod() == WifiEnterpriseConfig.Eap.TTLS
|| config.enterpriseConfig.getEapMethod() == WifiEnterpriseConfig.Eap.PEAP) {
config.enterpriseConfig.setPhase2Method(WifiEnterpriseConfig.Phase2.NONE);
config.enterpriseConfig.setIdentity(null);
if (TextUtils.isEmpty(config.enterpriseConfig.getAnonymousIdentity())) {
/**
* If anonymous identity was not provided, use "anonymous" to prevent any
* untrusted server from tracking real user identities.
*/
config.enterpriseConfig.setAnonymousIdentity(TOFU_ANONYMOUS_IDENTITY);
}
config.enterpriseConfig.setPassword(null);
}
}
mCurrentTofuConfig = config;
mServerCertChain.clear();
dismissDialogAndNotification();
registerCertificateNotificationReceiver();
if (useTrustOnFirstUse()) {
// Remove cached PMK in the framework and supplicant to avoid skipping the EAP flow
// only when TOFU is in use.
clearNativeData();
Log.d(TAG, "Remove native cached data and networks for TOFU.");
}
}
/**
* Do necessary clean up on stopping client mode.
*/
public void cleanup() {
dismissDialogAndNotification();
unregisterCertificateNotificationReceiver();
clearInternalData();
}
/**
* Stores a received certificate 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 certInfo a certificate info object from the server.
* @return true if the cert is cached; otherwise, false.
*/
public boolean addPendingCertificate(@NonNull String ssid, int depth,
@NonNull CertificateEventInfo certInfo) {
String configProfileKey = mCurrentTofuConfig != null
? mCurrentTofuConfig.getProfileKey() : "null";
if (TextUtils.isEmpty(ssid)) return false;
if (null == mCurrentTofuConfig) return false;
if (!TextUtils.equals(ssid, mCurrentTofuConfig.SSID)) return false;
if (null == certInfo) return false;
if (depth < 0) return false;
// If TOFU is not supported return immediately, although this should not happen since
// the caller code flow is only active when TOFU is supported.
if (!mIsTrustOnFirstUseSupported) return false;
// If insecure configurations are allowed and this configuration is configured with
// "Do not validate" (i.e. TOFU is disabled), skip loading the certificates (no need for
// them anyway) and don't disconnect the network.
if (mIsInsecureEnterpriseConfigurationAllowed
&& !mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
Log.d(TAG, "Certificates are not required for this connection");
return false;
}
if (depth == 0) {
// Disable network selection upon receiving the server certificate
putNetworkOnHold();
}
if (!mServerCertChain.contains(certInfo.getCert())) {
mServerCertChain.addFirst(certInfo.getCert());
Log.d(TAG, "addPendingCertificate: " + "SSID=" + ssid + " depth=" + depth
+ " certHash=" + certInfo.getCertHash() + " current config=" + configProfileKey
+ "\ncertificate content:\n" + certInfo.getCert());
}
// 0 is the tail, i.e. the server cert.
if (depth == 0 && null == mPendingServerCert) {
mPendingServerCert = certInfo.getCert();
mPendingServerCertSubjectInfo = CertificateSubjectInfo.parse(
certInfo.getCert().getSubjectX500Principal().getName());
if (null == mPendingServerCertSubjectInfo) {
Log.e(TAG, "Cert has no valid subject.");
return false;
}
mPendingServerCertIssuerInfo = CertificateSubjectInfo.parse(
certInfo.getCert().getIssuerX500Principal().getName());
if (null == mPendingServerCertIssuerInfo) {
Log.e(TAG, "Cert has no valid issuer.");
return false;
}
mServerCertHash = certInfo.getCertHash();
}
// Root or intermediate cert.
if (depth < mPendingRootCaCertDepth) {
return true;
}
mPendingRootCaCertDepth = depth;
mPendingRootCaCert = certInfo.getCert();
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 user approval dialog is displayed and the network is pending.
*/
public boolean startUserApprovalIfNecessary(boolean isUserSelected) {
if (null == mConnectingConfig || null == mCurrentTofuConfig) return false;
if (mConnectingConfig.networkId != mCurrentTofuConfig.networkId) 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. This should
// not happen because the TOFU flag will be set during boot if these conditions are met.
if (mIsTrustOnFirstUseSupported && !mIsInsecureEnterpriseConfigurationAllowed
&& !mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled()) {
Log.e(TAG, "Upgrade insecure connection to TOFU.");
mCurrentTofuConfig.enterpriseConfig.enableTrustOnFirstUse(true);
}
if (useTrustOnFirstUse()) {
if (null == mPendingRootCaCert) {
Log.e(TAG, "No valid CA cert for TLS-based connection.");
handleError(mCurrentTofuConfig.SSID);
return false;
}
if (null == mPendingServerCert) {
Log.e(TAG, "No valid Server cert for TLS-based connection.");
handleError(mCurrentTofuConfig.SSID);
return false;
}
Log.d(TAG, "TOFU certificate chain:");
for (X509Certificate cert : mServerCertChain) {
Log.d(TAG, cert.getSubjectX500Principal().getName());
}
if (!configureServerValidationMethod()) {
Log.e(TAG, "Server cert chain is invalid.");
String ssid = mCurrentTofuConfig.SSID;
handleError(ssid);
createCertificateErrorNotification(isUserSelected, ssid);
return false;
}
} else if (mIsInsecureEnterpriseConfigurationAllowed) {
Log.i(TAG, "Insecure networks without a Root CA cert are allowed.");
return false;
}
if (isUserSelected) {
askForUserApprovalForCaCertificate();
} else {
notifyUserForCaCertificate();
}
return true;
}
/**
* Create a notification or a dialog when a server certificate is invalid
*/
private void createCertificateErrorNotification(boolean isUserSelected, String ssid) {
String title = mContext.getString(R.string.wifi_tofu_invalid_cert_chain_title, ssid);
String message = mContext.getString(R.string.wifi_tofu_invalid_cert_chain_message);
String okButtonText = mContext.getString(
R.string.wifi_tofu_invalid_cert_chain_ok_text);
if (TextUtils.isEmpty(title) || TextUtils.isEmpty(message)) return;
if (isUserSelected) {
mTofuAlertDialog = mWifiDialogManager.createLegacySimpleDialog(
title,
message,
null /* positiveButtonText */,
null /* negativeButtonText */,
okButtonText,
new WifiDialogManager.SimpleDialogCallback() {
@Override
public void onPositiveButtonClicked() {
// Not used.
}
@Override
public void onNegativeButtonClicked() {
// Not used.
}
@Override
public void onNeutralButtonClicked() {
// Not used.
}
@Override
public void onCancelled() {
// Not used.
}
},
new WifiThreadRunner(mHandler));
mTofuAlertDialog.launchDialog();
} else {
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(message)
.setStyle(new Notification.BigTextStyle().bigText(message))
.setColor(mContext.getResources().getColor(
android.R.color.system_notification_accent_color));
mNotificationManager.notify(SystemMessage.NOTE_SERVER_CA_CERTIFICATE,
builder.build());
}
}
/**
* Disable network selection, disconnect if necessary, and clear PMK cache
*/
private void putNetworkOnHold() {
// Disable network selection upon receiving the server certificate
mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
WifiConfiguration.NetworkSelectionStatus
.DISABLED_BY_WIFI_MANAGER);
// Force disconnect and clear PMK cache to avoid supplicant reconnection
mWifiNative.disconnect(mInterfaceName);
clearNativeData();
}
/**
* Configure the server validation method based on the incoming server certificate chain.
* If a valid method is found, the method returns true, and the caller can continue the TOFU
* process.
*
* A valid method could be one of the following:
* 1. If only the leaf or a partial chain is provided, use server certificate pinning.
* 2. If a full chain is provided, use the provided Root CA, but only if we are able to
* cryptographically validate it.
*
* If no certificates were received, or the certificates are invalid, or chain verification
* fails, the method returns false and the caller should abort the TOFU process.
*/
private boolean configureServerValidationMethod() {
if (mServerCertChain.size() == 0) {
Log.e(TAG, "No certificate chain provided by the server.");
return false;
}
if (mServerCertChain.size() == 1) {
Log.i(TAG, "Only one certificate provided, use server certificate pinning");
return true;
}
if (mPendingRootCaCert.getSubjectX500Principal().getName()
.equals(mPendingRootCaCert.getIssuerX500Principal().getName())) {
if (mPendingRootCaCert.getVersion() >= 2
&& mPendingRootCaCert.getBasicConstraints() < 0) {
Log.i(TAG, "Root CA with no CA bit set in basic constraints, "
+ "use server certificate pinning");
return true;
}
} else {
// TODO: b/271921032 some deployments that use globally trusted Root CAs do not include
// the Root during the handshake, only an intermediate. We can start the handshake with
// the Android trust store and validate the connection with a Root CA rather than
// certificate pinning.
Log.i(TAG, "Root CA is not self-signed, use server certificate pinning");
return true;
}
CertPath certPath;
try {
certPath = CertificateFactory.getInstance("X.509").generateCertPath(mServerCertChain);
} catch (CertificateException e) {
Log.e(TAG, "Certificate chain is invalid.");
return false;
} catch (IllegalStateException e) {
Log.wtf(TAG, "Fail: " + e);
return false;
}
CertPathValidator certPathValidator;
try {
certPathValidator = CertPathValidator.getInstance("PKIX");
} catch (NoSuchAlgorithmException e) {
Log.wtf(TAG, "PKIX algorithm not supported.");
return false;
}
try {
Set<TrustAnchor> anchorSet = Set.of(new TrustAnchor(mPendingRootCaCert, null));
PKIXParameters params = new PKIXParameters(anchorSet);
params.setRevocationEnabled(false);
certPathValidator.validate(certPath, params);
} catch (InvalidAlgorithmParameterException e) {
Log.wtf(TAG, "Invalid algorithm exception.");
return false;
} catch (CertPathValidatorException e) {
Log.e(TAG, "Server certificate chain validation failed: " + e);
return false;
}
Log.i(TAG, "Server certificate chain validation succeeded, use Root CA");
mServerCertHash = null;
return true;
}
private boolean useTrustOnFirstUse() {
return mIsTrustOnFirstUseSupported
&& mCurrentTofuConfig.enterpriseConfig.isTrustOnFirstUseEnabled();
}
private void registerCertificateNotificationReceiver() {
unregisterCertificateNotificationReceiver();
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(mCurrentTofuConfig.networkId, true);
} else {
if (null == mPendingRootCaCert || null == mPendingServerCert) {
handleError(ssid);
return;
}
if (!mWifiConfigManager.updateCaCertificate(
mCurrentTofuConfig.networkId, mPendingRootCaCert, mPendingServerCert,
mServerCertHash)) {
// The user approved this network,
// keep the connection regardless of the result.
Log.e(TAG, "Cannot update CA cert to network " + mCurrentTofuConfig.getProfileKey()
+ ", CA cert = " + mPendingRootCaCert);
}
}
int networkId = mCurrentTofuConfig.networkId;
mWifiConfigManager.updateNetworkSelectionStatus(networkId,
WifiConfiguration.NetworkSelectionStatus.DISABLED_NONE);
dismissDialogAndNotification();
clearInternalData();
if (null != mCallbacks) mCallbacks.onAccept(ssid, networkId);
}
@VisibleForTesting
void handleReject(@NonNull String ssid) {
if (!isConnectionValid(ssid)) return;
boolean disconnectRequired = !useTrustOnFirstUse();
mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WIFI_MANAGER);
dismissDialogAndNotification();
clearInternalData();
if (disconnectRequired) clearNativeData();
if (null != mCallbacks) mCallbacks.onReject(ssid, disconnectRequired);
}
private void handleError(@Nullable String ssid) {
if (mCurrentTofuConfig != null) {
mWifiConfigManager.updateNetworkSelectionStatus(mCurrentTofuConfig.networkId,
WifiConfiguration.NetworkSelectionStatus
.DISABLED_BY_WIFI_MANAGER);
}
dismissDialogAndNotification();
clearInternalData();
clearNativeData();
if (null != mCallbacks) mCallbacks.onError(ssid);
}
private void askForUserApprovalForCaCertificate() {
if (mCurrentTofuConfig == null || TextUtils.isEmpty(mCurrentTofuConfig.SSID)) return;
if (useTrustOnFirstUse()) {
if (null == mPendingRootCaCert || 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;
String messageUrl = null;
int messageUrlStart = 0;
int messageUrlEnd = 0;
if (useTrustOnFirstUse()) {
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,
mPendingServerCertSubjectInfo.commonName))
.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_issuer_name_text,
mPendingServerCertIssuerInfo.commonName));
if (!TextUtils.isEmpty(mPendingServerCertSubjectInfo.organization)) {
contentBuilder.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_organization_text,
mPendingServerCertSubjectInfo.organization));
}
final Date expiration = mPendingServerCert.getNotAfter();
if (expiration != null) {
contentBuilder.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_expiration_text,
DateFormat.getMediumDateFormat(mContext).format(expiration)));
}
final String fingerprint = getDigest(mPendingServerCert, "SHA256");
if (!TextUtils.isEmpty(fingerprint)) {
contentBuilder.append(mContext.getString(
R.string.wifi_ca_cert_dialog_message_signature_name_text, fingerprint));
}
message = contentBuilder.toString();
} else {
String hint = mContext.getString(
R.string.wifi_ca_cert_dialog_preT_message_hint, mCurrentTofuConfig.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.createLegacySimpleDialogWithUrl(
title,
message,
messageUrl,
messageUrlStart,
messageUrlEnd,
positiveButtonText,
negativeButtonText,
null /* neutralButtonText */,
new WifiDialogManager.SimpleDialogCallback() {
@Override
public void onPositiveButtonClicked() {
if (mCurrentTofuConfig == null) {
return;
}
Log.d(TAG, "User accepted the server certificate");
handleAccept(mCurrentTofuConfig.SSID);
}
@Override
public void onNegativeButtonClicked() {
if (mCurrentTofuConfig == null) {
return;
}
Log.d(TAG, "User rejected the server certificate");
handleReject(mCurrentTofuConfig.SSID);
}
@Override
public void onNeutralButtonClicked() {
// Not used.
if (mCurrentTofuConfig == null) {
return;
}
Log.d(TAG, "User input neutral");
handleReject(mCurrentTofuConfig.SSID);
}
@Override
public void onCancelled() {
if (mCurrentTofuConfig == null) {
return;
}
Log.d(TAG, "User input canceled");
handleReject(mCurrentTofuConfig.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 (mCurrentTofuConfig == null) return;
if (useTrustOnFirstUse()) {
if (null == mPendingRootCaCert) return;
if (null == mPendingServerCert) return;
}
dismissDialogAndNotification();
PendingIntent tapPendingIntent;
if (useTrustOnFirstUse()) {
tapPendingIntent = genCaCertNotifIntent(ACTION_CERT_NOTIF_TAP, mCurrentTofuConfig.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,
mCurrentTofuConfig.SSID)
: mContext.getString(R.string.wifi_ca_cert_notification_preT_message,
mCurrentTofuConfig.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, mCurrentTofuConfig.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, mCurrentTofuConfig.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() {
mPendingRootCaCertDepth = -1;
mPendingRootCaCert = null;
mPendingServerCert = null;
mPendingServerCertSubjectInfo = null;
mPendingServerCertIssuerInfo = null;
mCurrentTofuConfig = null;
mServerCertHash = null;
}
private void clearNativeData() {
// PMK should be cleared or it would skip EAP flow next time.
if (null != mCurrentTofuConfig) {
mWifiNative.removeNetworkCachedData(mCurrentTofuConfig.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 == mCurrentTofuConfig) {
handleError(null);
return false;
}
if (!TextUtils.equals(ssid, mCurrentTofuConfig.SSID)) {
Log.w(TAG, "Target SSID " + mCurrentTofuConfig.SSID
+ " is different from TOFU returned SSID" + ssid);
return false;
}
return true;
}
@VisibleForTesting
static String getDigest(X509Certificate x509Certificate, String algorithm) {
if (x509Certificate == null) {
return "";
}
try {
byte[] bytes = x509Certificate.getEncoded();
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(bytes);
return fingerprint(digest);
} catch (CertificateEncodingException ignored) {
return "";
} catch (NoSuchAlgorithmException ignored) {
return "";
}
}
private static String fingerprint(byte[] bytes) {
if (bytes == null) {
return "";
}
StringJoiner sj = new StringJoiner(":");
for (byte b : bytes) {
sj.add(HexDump.toHexString(b));
}
return sj.toString();
}
/** 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.
* @param networkId network ID
*/
public void onAccept(@NonNull String ssid, int networkId) {}
/**
* When a certificate is rejected, this callback is called.
*
* @param ssid SSID of the network.
* @param disconnectRequired Set to true if the network is currently connected
*/
public void onReject(@NonNull String ssid, boolean disconnectRequired) {}
/**
* 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) {}
}
}