Merge "Safely remove Wi-Fi user credentials"
This commit is contained in:
committed by
Android (Google) Code Review
commit
11a4853410
@@ -39,6 +39,22 @@
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/credential_being_used_by_title"
|
||||
android:text="@string/credential_being_used_by"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/credential_being_used_by_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorTertiary"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contents_title"
|
||||
android:text="@string/credential_contains"
|
||||
|
@@ -5258,8 +5258,10 @@
|
||||
<string name="credentials_settings_not_available">Credentials are not available for this user</string>
|
||||
<!-- Sub-heading for a user credential installed to be used by apps and as part of VPN configurations. [CHAR LIMIT=NONE] -->
|
||||
<string name="credential_for_vpn_and_apps">Installed for VPN and apps</string>
|
||||
<!-- Sub-heading for a user credential installed for Wi-Fi configuration. [CHAR LIMIT=NONE]. -->
|
||||
<string name="credential_for_wifi">Installed for Wi\u2011Fi</string>
|
||||
<!-- Sub-heading for a user credential installed to be used as part of a Wi-Fi configuration. [CHAR LIMIT=NONE]. -->
|
||||
<string name="credential_for_wifi">Installed for Wi-Fi</string>
|
||||
<string name="credential_for_wifi_in_use">Installed for Wi\u2011Fi (In use)</string>
|
||||
<!-- Description of dialog to reset credential storage [CHAR LIMIT=NONE] -->
|
||||
<string name="credentials_reset_hint">Remove all the contents?</string>
|
||||
<!-- Toast message [CHAR LIMIT=30] -->
|
||||
@@ -5809,14 +5811,16 @@
|
||||
<!-- Alert dialog confirmation when removing a user CA certificate. -->
|
||||
<string name="trusted_credentials_remove_confirmation">Permanently remove the user CA certificate?</string>
|
||||
|
||||
<!-- Header for a list of items that a credential entry is required. For example, a network uses this credential. [CHAR LIMIT=NONE] -->
|
||||
<string name="credential_being_used_by">Being used by</string>
|
||||
<!-- Header for a list of items that a credential entry contains. For example, one private key and one certificate. [CHAR LIMIT=NONE] -->
|
||||
<string name="credential_contains">This entry contains:</string>
|
||||
<string name="credential_contains">This entry contains</string>
|
||||
<!-- Item found in the PKCS12 keystore being investigated [CHAR LIMIT=NONE] -->
|
||||
<string name="one_userkey">one user key</string>
|
||||
<string name="one_userkey">1 user key</string>
|
||||
<!-- Item found in the PKCS12 keystore being investigated [CHAR LIMIT=NONE] -->
|
||||
<string name="one_usercrt">one user certificate</string>
|
||||
<string name="one_usercrt">1 user certificate</string>
|
||||
<!-- Item found in the PKCS12 keystore being investigated [CHAR LIMIT=NONE] -->
|
||||
<string name="one_cacrt">one CA certificate</string>
|
||||
<string name="one_cacrt">1 CA certificate</string>
|
||||
<!-- Item found in thee PKCS12 keystore being investigated [CHAR LIMIT=NONE]-->
|
||||
<string name="n_cacrts">%d CA certificates</string>
|
||||
<!-- Alert dialog when viewing a set of user credentials. -->
|
||||
|
@@ -43,12 +43,14 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
|
||||
import com.android.settings.wifi.helper.SavedWifiHelper;
|
||||
import com.android.settingslib.RestrictedLockUtils;
|
||||
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
|
||||
import com.android.settingslib.RestrictedLockUtilsInternal;
|
||||
@@ -74,6 +76,9 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
|
||||
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
|
||||
|
||||
@VisibleForTesting
|
||||
protected SavedWifiHelper mSavedWifiHelper;
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
return SettingsEnums.USER_CREDENTIALS;
|
||||
@@ -88,15 +93,23 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Credential item = (Credential) view.getTag();
|
||||
if (item != null) {
|
||||
CredentialDialogFragment.show(this, item);
|
||||
if (item == null) return;
|
||||
if (item.isInUse()) {
|
||||
item.setUsedByNames(mSavedWifiHelper.getCertificateNetworkNames(item.alias));
|
||||
}
|
||||
showCredentialDialogFragment(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getActivity().setTitle(R.string.user_credentials);
|
||||
mSavedWifiHelper = SavedWifiHelper.getInstance(getContext(), getSettingsLifecycle());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void showCredentialDialogFragment(Credential item) {
|
||||
CredentialDialogFragment.show(this, item);
|
||||
}
|
||||
|
||||
protected void announceRemoval(String alias) {
|
||||
@@ -112,7 +125,9 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
}
|
||||
}
|
||||
|
||||
public static class CredentialDialogFragment extends InstrumentedDialogFragment {
|
||||
/** The fragment to show the credential information. */
|
||||
public static class CredentialDialogFragment extends InstrumentedDialogFragment
|
||||
implements DialogInterface.OnShowListener {
|
||||
private static final String TAG = "CredentialDialogFragment";
|
||||
private static final String ARG_CREDENTIAL = "credential";
|
||||
|
||||
@@ -162,17 +177,23 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
dialog.dismiss();
|
||||
}
|
||||
};
|
||||
// TODO: b/127865361
|
||||
// a safe means of clearing wifi certificates. Configs refer to aliases
|
||||
// directly so deleting certs will break dependent access points.
|
||||
// However, Wi-Fi used to remove this certificate from storage if the network
|
||||
// was removed, regardless if it is used in more than one network.
|
||||
// It has been decided to allow removing certificates from this menu, as we
|
||||
// assume that the user who manually adds certificates must have a way to
|
||||
// manually remove them.
|
||||
builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener);
|
||||
}
|
||||
return builder.create();
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.setOnShowListener(this);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override for the negative button enablement on demand.
|
||||
*/
|
||||
@Override
|
||||
public void onShow(DialogInterface dialogInterface) {
|
||||
final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
|
||||
if (item.isInUse()) {
|
||||
((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEGATIVE)
|
||||
.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -300,6 +321,9 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
while (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
Credential c = new Credential(alias, uid);
|
||||
if (!c.isSystem()) {
|
||||
c.setInUse(mSavedWifiHelper.isCertificateInUse(alias));
|
||||
}
|
||||
Key key = null;
|
||||
try {
|
||||
key = keyStore.getKey(alias, null);
|
||||
@@ -423,12 +447,13 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
}
|
||||
|
||||
((TextView) view.findViewById(R.id.alias)).setText(item.alias);
|
||||
((TextView) view.findViewById(R.id.purpose)).setText(item.isSystem()
|
||||
? R.string.credential_for_vpn_and_apps
|
||||
: R.string.credential_for_wifi);
|
||||
updatePurposeView(view.findViewById(R.id.purpose), item);
|
||||
|
||||
view.findViewById(R.id.contents).setVisibility(expanded ? View.VISIBLE : View.GONE);
|
||||
if (expanded) {
|
||||
updateUsedByViews(view.findViewById(R.id.credential_being_used_by_title),
|
||||
view.findViewById(R.id.credential_being_used_by_content), item);
|
||||
|
||||
for (int i = 0; i < credentialViewTypes.size(); i++) {
|
||||
final View detail = view.findViewById(credentialViewTypes.keyAt(i));
|
||||
detail.setVisibility(item.storedTypes.contains(credentialViewTypes.valueAt(i))
|
||||
@@ -438,6 +463,30 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
return view;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected static void updatePurposeView(TextView purpose, Credential item) {
|
||||
int subTextResId = R.string.credential_for_vpn_and_apps;
|
||||
if (!item.isSystem()) {
|
||||
subTextResId = (item.isInUse())
|
||||
? R.string.credential_for_wifi_in_use
|
||||
: R.string.credential_for_wifi;
|
||||
}
|
||||
purpose.setText(subTextResId);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected static void updateUsedByViews(TextView title, TextView content, Credential item) {
|
||||
List<String> usedByNames = item.getUsedByNames();
|
||||
if (usedByNames.size() > 0) {
|
||||
title.setVisibility(View.VISIBLE);
|
||||
content.setText(String.join("\n", usedByNames));
|
||||
content.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
title.setVisibility(View.GONE);
|
||||
content.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
static class AliasEntry {
|
||||
public String alias;
|
||||
public int uid;
|
||||
@@ -468,6 +517,16 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
*/
|
||||
final int uid;
|
||||
|
||||
/**
|
||||
* Indicate whether or not this credential is in use.
|
||||
*/
|
||||
boolean mIsInUse;
|
||||
|
||||
/**
|
||||
* The list of networks which use this credential.
|
||||
*/
|
||||
List<String> mUsedByNames = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Should contain some non-empty subset of:
|
||||
* <ul>
|
||||
@@ -524,10 +583,28 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment
|
||||
return UserHandle.getAppId(uid) == Process.SYSTEM_UID;
|
||||
}
|
||||
|
||||
public String getAlias() { return alias; }
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public EnumSet<Type> getStoredTypes() {
|
||||
return storedTypes;
|
||||
}
|
||||
|
||||
public void setInUse(boolean inUse) {
|
||||
mIsInUse = inUse;
|
||||
}
|
||||
|
||||
public boolean isInUse() {
|
||||
return mIsInUse;
|
||||
}
|
||||
|
||||
public void setUsedByNames(List<String> names) {
|
||||
mUsedByNames = new ArrayList<>(names);
|
||||
}
|
||||
|
||||
public List<String> getUsedByNames() {
|
||||
return new ArrayList<String>(mUsedByNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.settings;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.os.Process;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.testutils.ResourcesUtils;
|
||||
import com.android.settings.wifi.helper.SavedWifiHelper;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class UserCredentialsSettingsTest {
|
||||
static final String TEST_ALIAS = "test_alias";
|
||||
static final String TEST_USER_BY_NAME = "test_used_by_name";
|
||||
|
||||
static final String TEXT_PURPOSE_SYSTEM = "credential_for_vpn_and_apps";
|
||||
static final String TEXT_PURPOSE_WIFI = "credential_for_wifi";
|
||||
static final String TEXT_PURPOSE_WIFI_IN_USE = "credential_for_wifi_in_use";
|
||||
|
||||
@Rule
|
||||
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
|
||||
@Spy
|
||||
final Context mContext = ApplicationProvider.getApplicationContext();
|
||||
@Mock
|
||||
SavedWifiHelper mSavedWifiHelper;
|
||||
@Mock
|
||||
View mView;
|
||||
|
||||
UserCredentialsSettings mSettings;
|
||||
UserCredentialsSettings.Credential mSysCredential =
|
||||
new UserCredentialsSettings.Credential(TEST_ALIAS, Process.SYSTEM_UID);
|
||||
UserCredentialsSettings.Credential mWifiCredential =
|
||||
new UserCredentialsSettings.Credential(TEST_ALIAS, Process.WIFI_UID);
|
||||
List<String> mUsedByNames = Arrays.asList(TEST_USER_BY_NAME);
|
||||
TextView mPurposeView = new TextView(ApplicationProvider.getApplicationContext());
|
||||
TextView mUsedByTitleView = new TextView(ApplicationProvider.getApplicationContext());
|
||||
TextView mUsedByContentView = new TextView(ApplicationProvider.getApplicationContext());
|
||||
|
||||
@Before
|
||||
@UiThreadTest
|
||||
public void setUp() {
|
||||
when(mSavedWifiHelper.isCertificateInUse(any(String.class))).thenReturn(false);
|
||||
when(mSavedWifiHelper.getCertificateNetworkNames(any(String.class)))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(mView.getTag()).thenReturn(mWifiCredential);
|
||||
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
mSettings = spy(new UserCredentialsSettings());
|
||||
when(mSettings.getContext()).thenReturn(mContext);
|
||||
mSettings.mSavedWifiHelper = mSavedWifiHelper;
|
||||
doNothing().when(mSettings)
|
||||
.showCredentialDialogFragment(any(UserCredentialsSettings.Credential.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onClick_noCredentialInTag_doNothing() {
|
||||
when(mView.getTag()).thenReturn(null);
|
||||
|
||||
mSettings.onClick(mView);
|
||||
|
||||
verify(mSavedWifiHelper, never()).getCertificateNetworkNames(any(String.class));
|
||||
verify(mSettings, never())
|
||||
.showCredentialDialogFragment(any(UserCredentialsSettings.Credential.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onClick_credentialInNotUse_notSetUsedByNamesThenShowDialog() {
|
||||
mWifiCredential.setInUse(false);
|
||||
when(mView.getTag()).thenReturn(mWifiCredential);
|
||||
|
||||
mSettings.onClick(mView);
|
||||
|
||||
verify(mSavedWifiHelper, never()).getCertificateNetworkNames(any(String.class));
|
||||
verify(mSettings)
|
||||
.showCredentialDialogFragment(any(UserCredentialsSettings.Credential.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void onClick_credentialInUse_setUsedByNamesThenShowDialog() {
|
||||
mWifiCredential.setInUse(true);
|
||||
when(mView.getTag()).thenReturn(mWifiCredential);
|
||||
when(mSavedWifiHelper.getCertificateNetworkNames(any(String.class)))
|
||||
.thenReturn(mUsedByNames);
|
||||
|
||||
mSettings.onClick(mView);
|
||||
|
||||
verify(mSavedWifiHelper).getCertificateNetworkNames(any(String.class));
|
||||
assertThat(mWifiCredential.getUsedByNames()).isEqualTo(mUsedByNames);
|
||||
verify(mSettings)
|
||||
.showCredentialDialogFragment(any(UserCredentialsSettings.Credential.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void updatePurposeView_getSystemCert_setTextCorrectly() {
|
||||
mSettings.updatePurposeView(mPurposeView, mSysCredential);
|
||||
|
||||
assertThat(mPurposeView.getText()).isEqualTo(getResString(TEXT_PURPOSE_SYSTEM));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void updatePurposeView_getWifiCert_setTextCorrectly() {
|
||||
mWifiCredential.setInUse(false);
|
||||
|
||||
mSettings.updatePurposeView(mPurposeView, mWifiCredential);
|
||||
|
||||
assertThat(mPurposeView.getText()).isEqualTo(getResString(TEXT_PURPOSE_WIFI));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void updatePurposeView_isWifiCertInUse_setTextCorrectly() {
|
||||
mWifiCredential.setInUse(true);
|
||||
|
||||
mSettings.updatePurposeView(mPurposeView, mWifiCredential);
|
||||
|
||||
assertThat(mPurposeView.getText()).isEqualTo(getResString(TEXT_PURPOSE_WIFI_IN_USE));
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void updateUsedByViews_noUsedByName_hideViews() {
|
||||
mWifiCredential.setUsedByNames(new ArrayList<>());
|
||||
|
||||
mSettings.updateUsedByViews(mUsedByTitleView, mUsedByContentView, mWifiCredential);
|
||||
|
||||
assertThat(mUsedByTitleView.getVisibility()).isEqualTo(View.GONE);
|
||||
assertThat(mUsedByContentView.getVisibility()).isEqualTo(View.GONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void updateUsedByViews_hasUsedByName_showViews() {
|
||||
mWifiCredential.setUsedByNames(mUsedByNames);
|
||||
|
||||
mSettings.updateUsedByViews(mUsedByTitleView, mUsedByContentView, mWifiCredential);
|
||||
|
||||
assertThat(mUsedByTitleView.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
assertThat(mUsedByContentView.getVisibility()).isEqualTo(View.VISIBLE);
|
||||
assertThat(mUsedByContentView.getText().toString().contains(TEST_USER_BY_NAME)).isTrue();
|
||||
}
|
||||
|
||||
static String getResString(String name) {
|
||||
return ResourcesUtils.getResourcesString(ApplicationProvider.getApplicationContext(), name);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user