diff options
-rw-r--r-- | packages/VpnDialogs/Android.bp | 7 | ||||
-rw-r--r-- | packages/VpnDialogs/res/values/strings.xml | 29 | ||||
-rw-r--r-- | packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java | 61 | ||||
-rw-r--r-- | packages/VpnDialogs/tests/Android.bp | 36 | ||||
-rw-r--r-- | packages/VpnDialogs/tests/AndroidManifest.xml | 31 | ||||
-rw-r--r-- | packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java | 154 |
6 files changed, 312 insertions, 6 deletions
diff --git a/packages/VpnDialogs/Android.bp b/packages/VpnDialogs/Android.bp index 05135b2bebf9..e4f80e22694b 100644 --- a/packages/VpnDialogs/Android.bp +++ b/packages/VpnDialogs/Android.bp @@ -23,10 +23,15 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +android_library { + name: "VpnDialogsLib", + srcs: ["src/**/*.java"], +} + android_app { name: "VpnDialogs", certificate: "platform", privileged: true, - srcs: ["src/**/*.java"], + static_libs: ["VpnDialogsLib"], platform_apis: true, } diff --git a/packages/VpnDialogs/res/values/strings.xml b/packages/VpnDialogs/res/values/strings.xml index f971a0916837..28e7272853c4 100644 --- a/packages/VpnDialogs/res/values/strings.xml +++ b/packages/VpnDialogs/res/values/strings.xml @@ -100,4 +100,33 @@ without any consequences. [CHAR LIMIT=20] --> <string name="dismiss">Dismiss</string> + <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs + into displaying what they want. The system will attempt to sanitize the label, and if the + label is deemed dangerous, then this string is used instead. The first argument is the + first 30 characters of the label, and the second argument is the package name of the app. + Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read + "My VPN app wants to set up a VPN connection...". If the label is very long, then, this + will be used to show "VerylongVPNlabel… (com.my.vpn.app) wants to set up a VPN + connection...". For this case, the code will refer to sanitized_vpn_label_with_ellipsis. + --> + <string name="sanitized_vpn_label_with_ellipsis"> + <xliff:g id="sanitized_vpn_label_with_ellipsis" example="My VPN app">%1$s</xliff:g>… ( + <xliff:g id="sanitized_vpn_label_with_ellipsis" example="com.my.vpn.app">%2$s</xliff:g>) + </string> + + <!-- Malicious VPN apps may provide very long labels or cunning HTML to trick the system dialogs + into displaying what they want. The system will attempt to sanitize the label, and if the + label is deemed dangerous, then this string is used instead. The first argument is the + label, and the second argument is the package name of the app. + Example : Normally a VPN app may be called "My VPN app" in which case the dialog will read + "My VPN app wants to set up a VPN connection...". If the VPN label contains HTML tag but + the length is not very long, the dialog will show "VpnLabelWith<br>HtmlTag + (com.my.vpn.app) wants to set up a VPN connection...". For this case, the code will refer + to sanitized_vpn_label. + --> + <string name="sanitized_vpn_label"> + <xliff:g id="sanitized_vpn_label" example="My VPN app">%1$s</xliff:g> ( + <xliff:g id="sanitized_vpn_label" example="com.my.vpn.app">%2$s</xliff:g>) + </string> + </resources> diff --git a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java index fb2367843fc1..a98d6d8e0217 100644 --- a/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java +++ b/packages/VpnDialogs/src/com/android/vpndialogs/ConfirmDialog.java @@ -33,6 +33,7 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AlertActivity; import com.android.internal.net.VpnConfig; @@ -40,12 +41,19 @@ public class ConfirmDialog extends AlertActivity implements DialogInterface.OnClickListener, ImageGetter { private static final String TAG = "VpnConfirm"; + // Usually the label represents the app name, 150 code points might be enough to display the app + // name, and 150 code points won't cover the warning message from VpnDialog. + @VisibleForTesting + static final int MAX_VPN_LABEL_LENGTH = 150; + @VpnManager.VpnType private final int mVpnType; private String mPackage; private VpnManager mVm; + private View mView; + public ConfirmDialog() { this(VpnManager.TYPE_VPN_SERVICE); } @@ -54,6 +62,43 @@ public class ConfirmDialog extends AlertActivity mVpnType = vpnType; } + /** + * This function will use the string resource to combine the VPN label and the package name. + * + * If the VPN label violates the length restriction, the first 30 code points of VPN label and + * the package name will be returned. Or return the VPN label and the package name directly if + * the VPN label doesn't violate the length restriction. + * + * The result will be something like, + * - ThisIsAVeryLongVpnAppNameWhich... (com.vpn.app) + * if the VPN label violates the length restriction. + * or + * - VpnLabelWith<br>HtmlTag (com.vpn.app) + * if the VPN label doesn't violate the length restriction. + * + */ + private String getSimplifiedLabel(String vpnLabel, String packageName) { + if (vpnLabel.codePointCount(0, vpnLabel.length()) > 30) { + return getString(R.string.sanitized_vpn_label_with_ellipsis, + vpnLabel.substring(0, vpnLabel.offsetByCodePoints(0, 30)), + packageName); + } + + return getString(R.string.sanitized_vpn_label, vpnLabel, packageName); + } + + @VisibleForTesting + protected String getSanitizedVpnLabel(String vpnLabel, String packageName) { + final String sanitizedVpnLabel = Html.escapeHtml(vpnLabel); + final boolean exceedMaxVpnLabelLength = sanitizedVpnLabel.codePointCount(0, + sanitizedVpnLabel.length()) > MAX_VPN_LABEL_LENGTH; + if (exceedMaxVpnLabelLength || !vpnLabel.equals(sanitizedVpnLabel)) { + return getSimplifiedLabel(sanitizedVpnLabel, packageName); + } + + return sanitizedVpnLabel; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -75,15 +120,16 @@ public class ConfirmDialog extends AlertActivity finish(); return; } - View view = View.inflate(this, R.layout.confirm, null); - ((TextView) view.findViewById(R.id.warning)).setText( - Html.fromHtml(getString(R.string.warning, getVpnLabel()), - this, null /* tagHandler */)); + mView = View.inflate(this, R.layout.confirm, null); + ((TextView) mView.findViewById(R.id.warning)).setText( + Html.fromHtml(getString(R.string.warning, getSanitizedVpnLabel( + getVpnLabel().toString(), mPackage)), + this /* imageGetter */, null /* tagHandler */)); mAlertParams.mTitle = getText(R.string.prompt); mAlertParams.mPositiveButtonText = getText(android.R.string.ok); mAlertParams.mPositiveButtonListener = this; mAlertParams.mNegativeButtonText = getText(android.R.string.cancel); - mAlertParams.mView = view; + mAlertParams.mView = mView; setupAlert(); getWindow().setCloseOnTouchOutside(false); @@ -92,6 +138,11 @@ public class ConfirmDialog extends AlertActivity button.setFilterTouchesWhenObscured(true); } + @VisibleForTesting + public CharSequence getWarningText() { + return ((TextView) mView.findViewById(R.id.warning)).getText(); + } + private CharSequence getVpnLabel() { try { return VpnConfig.getVpnLabel(this, mPackage); diff --git a/packages/VpnDialogs/tests/Android.bp b/packages/VpnDialogs/tests/Android.bp new file mode 100644 index 000000000000..68639bd1c4fe --- /dev/null +++ b/packages/VpnDialogs/tests/Android.bp @@ -0,0 +1,36 @@ +// Copyright 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "VpnDialogsTests", + // Use platform certificate because the test will invoke a hidden API. + // (e.g. VpnManager#prepareVpn()). + certificate: "platform", + libs: [ + "android.test.runner", + "android.test.base", + ], + static_libs: [ + "androidx.test.core", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-target-minus-junit4", + "VpnDialogsLib", + ], + srcs: ["src/**/*.java"], +} diff --git a/packages/VpnDialogs/tests/AndroidManifest.xml b/packages/VpnDialogs/tests/AndroidManifest.xml new file mode 100644 index 000000000000..f26c1fecb4e3 --- /dev/null +++ b/packages/VpnDialogs/tests/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * 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. + */ +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.vpndialogs.tests"> + + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + <activity android:name="com.android.vpndialogs.VpnDialogTest$InstrumentedConfirmDialog"/> + </application> + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.vpndialogs.tests" + android:label="Vpn dialog tests"> + </instrumentation> +</manifest> diff --git a/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java new file mode 100644 index 000000000000..7cfa466ac961 --- /dev/null +++ b/packages/VpnDialogs/tests/src/com/android/vpndialogs/VpnDialogTest.java @@ -0,0 +1,154 @@ +/* + * 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.vpndialogs; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.VpnManager; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class VpnDialogTest { + private ActivityScenario<ConfirmDialog> mActivityScenario; + + @SuppressWarnings("StaticMockMember") + @Mock + private static PackageManager sPm; + + @SuppressWarnings("StaticMockMember") + @Mock + private static VpnManager sVm; + + @Mock + private ApplicationInfo mAi; + + private static final String VPN_APP_NAME = "VpnApp"; + private static final String VPN_APP_PACKAGE_NAME = "com.android.vpndialogs.VpnDialogTest"; + private static final String VPN_LABEL_CONTAINS_HTML_TAG = + "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a>"; + private static final String VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION = + "<b><a href=\"https://www.malicious.vpn.app.com\">Google Play</a></b>" + + " Wants to connect the network. <br></br><br></br><br></br><br></br><br></br>" + + " <br></br><br></br><br></br><br></br><br></br><br></br><br></br><br></br> Deny it?"; + private static final String VPN_LABEL_VIOLATES_LENGTH_RESTRICTION = "This is a VPN label" + + " which violates the length restriction. The length restriction here are 150 code" + + " points. So the VPN label should be sanitized, and shows the package name to the" + + " user."; + + public static class InstrumentedConfirmDialog extends ConfirmDialog { + @Override + public PackageManager getPackageManager() { + return sPm; + } + + @Override + public @Nullable Object getSystemService(@ServiceName @NonNull String name) { + switch (name) { + case Context.VPN_MANAGEMENT_SERVICE: + return sVm; + default: + return super.getSystemService(name); + } + } + + @Override + public String getCallingPackage() { + return VPN_APP_PACKAGE_NAME; + } + } + + private void launchActivity() { + final Context context = getInstrumentation().getContext(); + mActivityScenario = ActivityScenario.launch( + new Intent(context, InstrumentedConfirmDialog.class)); + } + + @Test + public void testGetSanitizedVpnLabel_withNormalCase() throws Exception { + // Test the normal case that the VPN label showed in the VpnDialog is the app name. + doReturn(VPN_APP_NAME).when(mAi).loadLabel(sPm); + launchActivity(); + mActivityScenario.onActivity(activity -> { + assertTrue(activity.getWarningText().toString().contains(VPN_APP_NAME)); + }); + } + + private void verifySanitizedVpnLabel(String originalLabel) { + doReturn(originalLabel).when(mAi).loadLabel(sPm); + launchActivity(); + mActivityScenario.onActivity(activity -> { + // The VPN label was sanitized because violating length restriction or having a html + // tag, so the warning message will contain the package name. + assertTrue(activity.getWarningText().toString().contains(activity.getCallingPackage())); + // Also, the length of sanitized VPN label shouldn't longer than MAX_VPN_LABEL_LENGTH + // and it shouldn't contain html tag. + final String sanitizedVpnLabel = + activity.getSanitizedVpnLabel(originalLabel, VPN_APP_PACKAGE_NAME); + assertTrue(sanitizedVpnLabel.codePointCount(0, sanitizedVpnLabel.length()) + < ConfirmDialog.MAX_VPN_LABEL_LENGTH); + assertFalse(sanitizedVpnLabel.contains("<b>")); + }); + } + + @Test + public void testGetSanitizedVpnLabel_withHtmlTag() throws Exception { + // Test the case that the VPN label was sanitized because there is a html tag. + verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG); + } + + @Test + public void testGetSanitizedVpnLabel_withHtmlTagAndViolateLengthRestriction() throws Exception { + // Test the case that the VPN label was sanitized because there is a html tag. + verifySanitizedVpnLabel(VPN_LABEL_CONTAINS_HTML_TAG_AND_VIOLATE_LENGTH_RESTRICTION); + } + + @Test + public void testGetSanitizedVpnLabel_withLengthRestriction() throws Exception { + // Test the case that the VPN label was sanitized because hitting the length restriction. + verifySanitizedVpnLabel(VPN_LABEL_VIOLATES_LENGTH_RESTRICTION); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + doReturn(false).when(sVm).prepareVpn(anyString(), anyString(), anyInt()); + doReturn(null).when(sPm).queryIntentServices(any(), anyInt()); + doReturn(mAi).when(sPm).getApplicationInfo(anyString(), anyInt()); + } +} |