/* * Copyright (C) 2017 Oasis Feng. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * 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.oasisfeng.condom; import android.Manifest.permission; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.UserHandle; import android.provider.Settings; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import org.junit.Assume; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.ParametersAreNonnullByDefault; import static android.content.Intent.FLAG_RECEIVER_REGISTERED_ONLY; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.HONEYCOMB_MR1; import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; import static android.os.Build.VERSION_CODES.N; import static android.os.Build.VERSION_CODES.O; import static java.util.Objects.requireNonNull; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; @ParametersAreNonnullByDefault public class CondomContextBlockingTest { @Test public void testSelfTargeted() { final TestContext context = new TestContext(); final CondomContext condom = CondomContext.wrap(context, TAG), dry_condom = CondomContext.wrap(context, TAG, new CondomOptions().setDryRun(true)); // Self-targeting test final String self_pkg = condom.getPackageName(); final Intent[] self_targeted_intents = new Intent[] { intent().setPackage(self_pkg), intent().setComponent(new ComponentName(self_pkg, "X")) }; for (final Context context2test : new Context[] {condom, condom.getApplicationContext(), dry_condom, dry_condom.getApplicationContext()}) with(self_targeted_intents, allBroadcastAndServiceApis(context2test), context.EXPECT_BASE_CALLED, context.expectFlags(0)); } @Test public void testPreventNone() { final TestContext context = new TestContext(); final CondomOptions options = new CondomOptions().preventServiceInBackgroundPackages(false).preventBroadcastToBackgroundPackages(false); final CondomContext condom = CondomContext.wrap(context, TAG, options), dry_condom = CondomContext.wrap(context, TAG, options.setDryRun(true)); //noinspection deprecation, intentional test for deprecated method condom.preventWakingUpStoppedPackages(false); //noinspection deprecation dry_condom.preventWakingUpStoppedPackages(false); for (final Context context2test : new Context[] {condom, condom.getApplicationContext(), dry_condom, dry_condom.getApplicationContext()}) with(ALL_SORT_OF_INTENTS, allBroadcastAndServiceApis(context2test), context.EXPECT_BASE_CALLED, context.expectFlags(0)); } @Test public void testPreventWakingUpStoppedPackages_IncludingDryRun() { final Intent[] intents_with_inc_stop = ALL_SORT_OF_INTENTS.clone(); for (int i = 0; i < intents_with_inc_stop.length; i++) intents_with_inc_stop[i] = new Intent(intents_with_inc_stop[i]).addFlags(FLAG_INCLUDE_STOPPED_PACKAGES); final TestContext context = new TestContext(); final CondomOptions options = new CondomOptions().preventBroadcastToBackgroundPackages(false).preventServiceInBackgroundPackages(false); final CondomContext condom = CondomContext.wrap(context, TAG, options), dry_condom = CondomContext.wrap(context, TAG, options.setDryRun(true)); for (final Context context2test : new Context[] {condom, condom.getApplicationContext()}) with(intents_with_inc_stop, allBroadcastAndServiceApis(context2test), context.EXPECT_BASE_CALLED, context.expectFlags(FLAG_EXCLUDE_STOPPED_PACKAGES)); for (final Context context2test : new Context[] {dry_condom, dry_condom.getApplicationContext()}) with(intents_with_inc_stop, allBroadcastAndServiceApis(context2test), context.EXPECT_BASE_CALLED, context.expectFlags(FLAG_INCLUDE_STOPPED_PACKAGES)); } @Test public void testPreventBroadcastToBackgroundPackages() { final TestContext context = new TestContext(); final CondomOptions options = new CondomOptions().preventBroadcastToBackgroundPackages(true); final CondomContext condom = CondomContext.wrap(context, TAG, options), dry_condom = CondomContext.wrap(context, TAG, options.setDryRun(true)); final int extra_flag = SDK_INT >= N ? CondomCore.FLAG_RECEIVER_EXCLUDE_BACKGROUND : FLAG_RECEIVER_REGISTERED_ONLY; for (final Context context2test : new Context[] {condom, condom.getApplicationContext()}) with(ALL_SORT_OF_INTENTS, allBroadcastApis(context2test), context.EXPECT_BASE_CALLED, context.expectFlags(FLAG_EXCLUDE_STOPPED_PACKAGES | extra_flag)); for (final Context context2test : new Context[] {dry_condom, dry_condom.getApplicationContext()}) with(ALL_SORT_OF_INTENTS, allBroadcastApis(context2test), context.EXPECT_BASE_CALLED, context.expectFlags(0)); } @Test public void testPreventServiceInBackgroundPackages() { Assume.assumeTrue(SDK_INT < O); final TestContext context = new TestContext(); context.mTestingBackgroundUid = true; final CondomOptions options = new CondomOptions().preventServiceInBackgroundPackages(true).preventBroadcastToBackgroundPackages(false); final CondomContext condom = CondomContext.wrap(context, TAG, options), dry_condom = CondomContext.wrap(context, TAG, options.setDryRun(true)); for (final Context context2test : new Context[] {condom, condom.getApplicationContext()}) { final List<ResolveInfo> services = context2test.getPackageManager().queryIntentServices(intent(), 0); assertEquals(3, services.size()); context.assertBaseCalled(); final ResolveInfo resolve = context2test.getPackageManager().resolveService(intent(), 0); assertEquals("non.bg.service", requireNonNull(resolve).serviceInfo.packageName); context.assertBaseCalled(); } for (final Context context2test : new Context[] {dry_condom, dry_condom.getApplicationContext()}) { final List<ResolveInfo> services = context2test.getPackageManager().queryIntentServices(intent(), 0); assertEquals(4, services.size()); context.assertBaseCalled(); assertEquals(7777777, requireNonNull(context2test.getPackageManager().resolveService(intent(), 0)) .serviceInfo.applicationInfo.uid); context.assertBaseCalled(); } } @Test public void testContentProviderOutboundJudge() { final TestContext context = new TestContext(); final CondomOptions options = new CondomOptions().setOutboundJudge((type, intent, target_pkg) -> { final PackageManager pm = ApplicationProvider.getApplicationContext().getPackageManager(); final String settings_pkg = requireNonNull(pm.resolveContentProvider(requireNonNull(Settings.System.CONTENT_URI.getAuthority()), 0)).packageName; return ! settings_pkg.equals(target_pkg); }); final CondomContext condom = CondomContext.wrap(context, TAG, options), dry_condom = CondomContext.wrap(context, TAG, options.setDryRun(true)); for (final Context context2test : new Context[] {condom, condom.getApplicationContext()}) { assertNull(context2test.getPackageManager().resolveContentProvider(Settings.AUTHORITY, 0)); assertNull(context2test.getContentResolver().acquireContentProviderClient(Settings.System.CONTENT_URI)); } for (final Context context2test : new Context[] {dry_condom, dry_condom.getApplicationContext()}) { assertNotNull(context2test.getPackageManager().resolveContentProvider(Settings.AUTHORITY, 0)); assertNotNull(context2test.getContentResolver().acquireContentProviderClient(Settings.System.CONTENT_URI)); } } @Test public void testContentProvider() { final TestContext context = new TestContext(); final CondomContext condom = CondomContext.wrap(context, TAG), dry_condom = CondomContext.wrap(context, TAG, new CondomOptions().setDryRun(true)); // Regular provider access final String android_id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); assertNotNull(android_id); for (final Context context2test : new Context[] {condom, condom.getApplicationContext(), dry_condom, dry_condom.getApplicationContext()}) { final String condom_android_id = Settings.Secure.getString(context2test.getContentResolver(), Settings.Secure.ANDROID_ID); assertEquals(android_id, condom_android_id); } context.mTestingStoppedProvider = true; for (final Context context2test : new Context[] {condom, condom.getApplicationContext()}) { // Prevent stopped packages, assertNull(context2test.getPackageManager().resolveContentProvider(TEST_AUTHORITY, 0)); assertNull(context2test.getContentResolver().acquireContentProviderClient(TEST_CONTENT_URI)); // Providers in system package should not be blocked. assertNotNull(context2test.getPackageManager().resolveContentProvider(Settings.AUTHORITY, 0)); assertNotNull(context2test.getContentResolver().acquireContentProviderClient(Settings.System.CONTENT_URI)); } for (final Context context2test : new Context[] {dry_condom, dry_condom.getApplicationContext()}) { assertNotNull(context2test.getPackageManager().resolveContentProvider(TEST_AUTHORITY, 0)); assertNotNull(context2test.getContentResolver().acquireContentProviderClient(TEST_CONTENT_URI)); } context.mTestingStoppedProvider = false; } private static final String TEST_AUTHORITY = "com.oasisfeng.condom.test"; private static final Uri TEST_CONTENT_URI = Uri.parse("content://" + TEST_AUTHORITY + "/"); public static class TestProvider extends ContentProvider { @Override public boolean onCreate() { return true; } @Nullable @Override public Cursor query(@NonNull final Uri uri, @Nullable final String[] strings, @Nullable final String s, @Nullable final String[] strings1, @Nullable final String s1) { return null; } @Nullable @Override public String getType(@NonNull final Uri uri) { return null; } @Nullable @Override public Uri insert(@NonNull final Uri uri, @Nullable final ContentValues contentValues) { return null; } @Override public int delete(@NonNull final Uri uri, @Nullable final String s, @Nullable final String[] strings) { return 0; } @Override public int update(@NonNull final Uri uri, @Nullable final ContentValues contentValues, @Nullable final String s, @Nullable final String[] strings) { return 0; } } @Test public void testOutboundJudge() { final TestContext context = new TestContext(); final CondomOptions options = new CondomOptions().setOutboundJudge((type, intent, target_pkg) -> { mNumOutboundJudgeCalled.incrementAndGet(); return ! DISALLOWED_PACKAGE.equals(target_pkg); }); final CondomContext condom = CondomContext.wrap(context, TAG, options), dry_condom = CondomContext.wrap(context, TAG, options.setDryRun(true)); final Runnable EXPECT_OUTBOUND_JUDGE_REFUSAL = () -> { context.assertBaseNotCalled(); assertOutboundJudgeCalled(1); }; final Runnable EXPECT_OUTBOUND_JUDGE_PASS = () -> { context.assertBaseCalled(); assertOutboundJudgeCalled(1); }; for (final Context context2test : new Context[] {condom, condom.getApplicationContext()}) with(DISALLOWED_INTENTS, allBroadcastApis(context2test), EXPECT_OUTBOUND_JUDGE_REFUSAL); for (final Context context2test : new Context[] {dry_condom, dry_condom.getApplicationContext()}) with(DISALLOWED_INTENTS, allBroadcastApis(context2test), EXPECT_OUTBOUND_JUDGE_PASS); for (final Context context2test : new Context[] {condom, condom.getApplicationContext(), dry_condom, dry_condom.getApplicationContext()}) with(ALLOWED_INTENTS, allBroadcastApis(context2test), EXPECT_OUTBOUND_JUDGE_PASS); final PackageManager pm = condom.getPackageManager(), dry_pm = dry_condom.getPackageManager(); assertNull(pm.resolveService(intent().setPackage(DISALLOWED_PACKAGE), 0)); context.assertBaseNotCalled(); assertOutboundJudgeCalled(1); assertNotNull(dry_pm.resolveService(intent().setPackage(DISALLOWED_PACKAGE), 0)); context.assertBaseCalled(); assertOutboundJudgeCalled(1); assertEquals(1, pm.queryIntentServices(intent(), 0).size()); context.assertBaseCalled(); assertOutboundJudgeCalled(2); assertEquals(2, dry_pm.queryIntentServices(intent(), 0).size()); context.assertBaseCalled(); assertOutboundJudgeCalled(2); assertEquals(1, pm.queryBroadcastReceivers(intent(), 0).size()); context.assertBaseCalled(); assertOutboundJudgeCalled(2); assertEquals(2, dry_pm.queryBroadcastReceivers(intent(), 0).size()); context.assertBaseCalled(); assertOutboundJudgeCalled(2); condom.sendBroadcast(intent()); context.assertBaseCalled(); assertOutboundJudgeCalled(0); dry_condom.sendBroadcast(intent()); context.assertBaseCalled(); assertOutboundJudgeCalled(0); } private static void with(final Intent[] intents, final Consumer<Intent>[] tests, final Runnable... expectations) { for (final Intent intent : intents) for (final Consumer<Intent> test : tests) { test.accept(intent); for (final Runnable expectation : expectations) expectation.run(); } } private static Intent intent() { return new Intent("com.example.TEST").addFlags(INTENT_FLAGS); } private static final UserHandle USER = SDK_INT >= JELLY_BEAN_MR1 ? android.os.Process.myUserHandle() : null; private static final int INTENT_FLAGS = Intent.FLAG_DEBUG_LOG_RESOLUTION | Intent.FLAG_FROM_BACKGROUND; // Just random flags to verify flags preservation. private static final ServiceConnection SERVICE_CONNECTION = new ServiceConnection() { @Override public void onServiceConnected(final ComponentName name, final IBinder service) {} @Override public void onServiceDisconnected(final ComponentName name) {} }; private static final String DISALLOWED_PACKAGE = "a.b.c"; private static final String ALLOWED_PACKAGE = "x.y.z"; private static final ComponentName DISALLOWED_COMPONENT = new ComponentName(DISALLOWED_PACKAGE, "A"); private static final ComponentName ALLOWED_COMPONENT = new ComponentName(ALLOWED_PACKAGE, "A"); private static final int FLAG_EXCLUDE_STOPPED_PACKAGES = SDK_INT >= HONEYCOMB_MR1 ? Intent.FLAG_EXCLUDE_STOPPED_PACKAGES : 0; private static final int FLAG_INCLUDE_STOPPED_PACKAGES = SDK_INT >= HONEYCOMB_MR1 ? Intent.FLAG_INCLUDE_STOPPED_PACKAGES : 0; private static final Intent[] ALL_SORT_OF_INTENTS = new Intent[] { intent(), intent().setPackage(ALLOWED_PACKAGE), intent().setPackage(DISALLOWED_PACKAGE), intent().setComponent(ALLOWED_COMPONENT), intent().setComponent(DISALLOWED_COMPONENT), }; private static final Intent[] ALLOWED_INTENTS = new Intent[] { intent().setPackage(ALLOWED_PACKAGE), intent().setComponent(ALLOWED_COMPONENT), }; private static final Intent[] DISALLOWED_INTENTS = new Intent[] { intent().setPackage(DISALLOWED_PACKAGE), intent().setComponent(DISALLOWED_COMPONENT), }; private static Consumer<Intent>[] allBroadcastAndServiceApis(final Context condom) { final Consumer<Intent>[] broadcast_apis = allBroadcastApis(condom); final Consumer<Intent>[] service_apis = allServiceApis(condom); final Consumer<Intent>[] all = Arrays.copyOf(broadcast_apis, broadcast_apis.length + service_apis.length); System.arraycopy(service_apis, 0, all, broadcast_apis.length, service_apis.length); return all; } private static Consumer<Intent>[] allBroadcastApis(final Context condom) { final List<Consumer<Intent>> tests = new ArrayList<>(); tests.add(condom::sendBroadcast); tests.add(intent -> condom.sendBroadcast(intent, permission.DUMP)); tests.add(intent -> condom.sendOrderedBroadcast(intent, permission.DUMP)); tests.add(intent -> condom.sendOrderedBroadcast(intent, permission.DUMP, null, null, 0, null, null)); tests.add(condom::sendStickyBroadcast); tests.add(intent -> condom.sendStickyOrderedBroadcast(intent, null, null, 0, null, null)); if (SDK_INT >= JELLY_BEAN_MR1) { tests.add(intent -> condom.sendBroadcastAsUser(intent, USER)); tests.add(intent -> condom.sendBroadcastAsUser(intent, USER, null)); tests.add(intent -> condom.sendStickyBroadcastAsUser(intent, USER)); tests.add(intent -> condom.sendOrderedBroadcastAsUser(intent, USER, null, null, null, 0, null, null)); tests.add(intent -> condom.sendStickyOrderedBroadcastAsUser(intent, USER,null, null, 0, null, null)); } tests.add(intent -> condom.getPackageManager().queryBroadcastReceivers(intent, 0)); //noinspection unchecked return tests.toArray(new Consumer[0]); } @SuppressWarnings("unchecked") private static Consumer<Intent>[] allServiceApis(final Context condom) { return new Consumer[] { (Consumer<Intent>) condom::startService, (Consumer<Intent>) intent -> condom.bindService(intent, SERVICE_CONNECTION, 0) }; } private void assertOutboundJudgeCalled(final int count) { assertEquals(count, mNumOutboundJudgeCalled.getAndSet(0)); } private final AtomicInteger mNumOutboundJudgeCalled = new AtomicInteger(); private static final String TAG = "Test"; private static class TestContext extends ContextWrapper { @CallSuper void check(final Intent intent) { assertBaseNotCalled(); mBaseCalled = true; mIntentFlags = intent.getFlags(); } @Override public ComponentName startService(final Intent intent) { check(intent); return null; } @Override public boolean bindService(final Intent intent, final ServiceConnection c, final int f) { check(intent); return false; } @Override public void sendBroadcast(final Intent intent) { check(intent); } @Override public void sendBroadcast(final Intent intent, final String p) { check(intent); } @Override public void sendBroadcastAsUser(final Intent intent, final UserHandle user) { check(intent); } @Override public void sendBroadcastAsUser(final Intent intent, final UserHandle user, final String receiverPermission) { check(intent); } @SuppressWarnings("deprecation") @Override public void sendStickyBroadcast(final Intent intent) { check(intent); } @SuppressWarnings("deprecation") @Override public void sendStickyBroadcastAsUser(final Intent intent, final UserHandle u) { check(intent); } @Override public void sendOrderedBroadcast(final Intent intent, final String p) { check(intent); } @Override public void sendOrderedBroadcast(final Intent intent, final String p, final BroadcastReceiver r, final Handler s, final int c, final String d, final Bundle e) { check(intent); } @Override public void sendOrderedBroadcastAsUser(final Intent intent, final UserHandle u, final String p, final BroadcastReceiver r, final Handler s, final int c, final String d, final Bundle e) { check(intent); } @SuppressWarnings("deprecation") @Override public void sendStickyOrderedBroadcast(final Intent intent, final BroadcastReceiver r, final Handler s, final int c, final String d, final Bundle e) { check(intent); } @SuppressWarnings("deprecation") @Override public void sendStickyOrderedBroadcastAsUser(final Intent intent, final UserHandle u, final BroadcastReceiver r, final Handler s, final int c, final String d, final Bundle e) { check(intent); } @Override public PackageManager getPackageManager() { return new PackageManagerWrapper(ApplicationProvider.getApplicationContext().getPackageManager()) { @Override public ResolveInfo resolveService(final Intent intent, final int flags) { check(intent); return buildResolveInfo(DISALLOWED_PACKAGE, true, 7777777); // Must be consistent with the first entry from queryIntentServices(). } @Override public @NonNull List<ResolveInfo> queryIntentServices(final Intent intent, final int flags) { check(intent); final List<ResolveInfo> resolves = new ArrayList<>(); if (mTestingBackgroundUid) { final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); Assume.assumeTrue(am != null); final List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(32); if (services != null) for (final ActivityManager.RunningServiceInfo service : services) { if (service.pid == 0 || service.uid == android.os.Process.myUid()) continue; resolves.add(buildResolveInfo(DISALLOWED_PACKAGE, true, 7777777)); // Simulate a background UID. resolves.add(buildResolveInfo("non.bg.service", true, service.uid)); break; } } resolves.add(buildResolveInfo(ALLOWED_PACKAGE, true, android.os.Process.myUid())); resolves.add(buildResolveInfo(DISALLOWED_PACKAGE, true, android.os.Process.myUid())); return resolves; } @Override public @NonNull List<ResolveInfo> queryBroadcastReceivers(final Intent intent, final int flags) { check(intent); final List<ResolveInfo> resolves = new ArrayList<>(); resolves.add(buildResolveInfo(ALLOWED_PACKAGE, false, android.os.Process.myUid())); resolves.add(buildResolveInfo(DISALLOWED_PACKAGE, false, android.os.Process.myUid())); return resolves; } @Override public ProviderInfo resolveContentProvider(final String name, final int flags) { final ProviderInfo info = super.resolveContentProvider(name, flags); if (info != null && mTestingStoppedProvider) { if (getPackageName().equals(info.packageName)) info.packageName += ".dummy"; // To simulate a package other than current one. info.applicationInfo.flags |= ApplicationInfo.FLAG_STOPPED; } return info; } private ResolveInfo buildResolveInfo(final String pkg, final boolean service_or_receiver, final int uid) { final ResolveInfo r = new ResolveInfo() { @Override public String toString() { return "ResolveInfo{test}"; } }; final ComponentInfo info = service_or_receiver ? (r.serviceInfo = new ServiceInfo()) : (r.activityInfo = new ActivityInfo()); info.packageName = pkg; info.applicationInfo = new ApplicationInfo(); info.applicationInfo.packageName = pkg; info.applicationInfo.uid = uid; return r; } }; } @Override public Context getApplicationContext() { return this; } void assertBaseCalled() { assertTrue(mBaseCalled); mBaseCalled = false; } void assertBaseNotCalled() { assertFalse(mBaseCalled); } Runnable expectFlags(final int flags) { return () -> assertEquals(flags | INTENT_FLAGS, mIntentFlags); } TestContext() { super(ApplicationProvider.getApplicationContext()); } boolean mTestingBackgroundUid; boolean mTestingStoppedProvider; private int mIntentFlags; private boolean mBaseCalled; final Runnable EXPECT_BASE_CALLED = this::assertBaseCalled; } private interface Consumer<T> { void accept(T t); } }