Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3772ff11 authored by Nino Jagar's avatar Nino Jagar
Browse files

Use groups in content protection processor

Bug: 302188072
Test: Unit tests and manual adb/test app
Change-Id: I7db9c44ba8cd9b77dcf9ffeca856fd495ec1a869
parent 29104e5e
Loading
Loading
Loading
Loading
+14 −5
Original line number Diff line number Diff line
@@ -317,16 +317,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
            }
        }

        // Should not be possible for mComponentName to be null here but check anyway
        if (mManager.mOptions.contentProtectionOptions.enableReceiver
                && mManager.getContentProtectionEventBuffer() != null
                && mComponentName != null) {
        if (isContentProtectionEnabled()) {
            mContentProtectionEventProcessor =
                    new ContentProtectionEventProcessor(
                            mManager.getContentProtectionEventBuffer(),
                            mHandler,
                            mSystemServerInterface,
                            mComponentName.getPackageName());
                            mComponentName.getPackageName(),
                            mManager.mOptions.contentProtectionOptions);
        } else {
            mContentProtectionEventProcessor = null;
        }
@@ -956,4 +954,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession {
    private boolean isContentCaptureReceiverEnabled() {
        return mManager.mOptions.enableReceiver;
    }

    @UiThread
    private boolean isContentProtectionEnabled() {
        // Should not be possible for mComponentName to be null here but check anyway
        // Should not be possible for groups to be empty if receiver is enabled but check anyway
        return mManager.mOptions.contentProtectionOptions.enableReceiver
                && mManager.getContentProtectionEventBuffer() != null
                && mComponentName != null
                && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty()
                        || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty());
    }
}
+64 −90
Original line number Diff line number Diff line
@@ -19,9 +19,9 @@ package android.view.contentprotection;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.ContentCaptureOptions;
import android.content.pm.ParceledListSlice;
import android.os.Handler;
import android.text.InputType;
import android.util.Log;
import android.view.contentcapture.ContentCaptureEvent;
import android.view.contentcapture.IContentCaptureManager;
@@ -33,10 +33,10 @@ import com.android.internal.util.RingBuffer;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

/**
 * Main entry point for processing {@link ContentCaptureEvent} for the content protection flow.
@@ -47,33 +47,13 @@ public class ContentProtectionEventProcessor {

    private static final String TAG = "ContentProtectionEventProcessor";

    private static final List<Integer> PASSWORD_FIELD_INPUT_TYPES =
            Collections.unmodifiableList(
                    Arrays.asList(
                            InputType.TYPE_NUMBER_VARIATION_PASSWORD,
                            InputType.TYPE_TEXT_VARIATION_PASSWORD,
                            InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
                            InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD));

    private static final List<String> PASSWORD_TEXTS =
            Collections.unmodifiableList(
                    Arrays.asList("password", "pass word", "code", "pin", "credential"));

    private static final List<String> ADDITIONAL_SUSPICIOUS_TEXTS =
            Collections.unmodifiableList(
                    Arrays.asList("user", "mail", "phone", "number", "login", "log in", "sign in"));

    private static final Duration MIN_DURATION_BETWEEN_FLUSHING = Duration.ofSeconds(3);

    private static final String ANDROID_CLASS_NAME_PREFIX = "android.";

    private static final Set<Integer> EVENT_TYPES_TO_STORE =
            Collections.unmodifiableSet(
                    new HashSet<>(
                            Arrays.asList(
            Set.of(
                    ContentCaptureEvent.TYPE_VIEW_APPEARED,
                    ContentCaptureEvent.TYPE_VIEW_DISAPPEARED,
                                    ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED)));
                    ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED);

    private static final int RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS = 150;

@@ -85,11 +65,7 @@ public class ContentProtectionEventProcessor {

    @NonNull private final String mPackageName;

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public boolean mPasswordFieldDetected = false;

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public boolean mSuspiciousTextDetected = false;
    @NonNull private final ContentCaptureOptions.ContentProtectionOptions mOptions;

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    @Nullable
@@ -97,15 +73,32 @@ public class ContentProtectionEventProcessor {

    private int mResetLoginRemainingEventsToProcess;

    private boolean mAnyGroupFound = false;

    // Ordered by priority
    private final List<SearchGroup> mGroupsRequired;

    // Ordered by priority
    private final List<SearchGroup> mGroupsOptional;

    // Ordered by priority
    private final List<SearchGroup> mGroupsAll;

    public ContentProtectionEventProcessor(
            @NonNull RingBuffer<ContentCaptureEvent> eventBuffer,
            @NonNull Handler handler,
            @NonNull IContentCaptureManager contentCaptureManager,
            @NonNull String packageName) {
            @NonNull String packageName,
            @NonNull ContentCaptureOptions.ContentProtectionOptions options) {
        mEventBuffer = eventBuffer;
        mHandler = handler;
        mContentCaptureManager = contentCaptureManager;
        mPackageName = packageName;
        mOptions = options;
        mGroupsRequired = options.requiredGroups.stream().map(SearchGroup::new).toList();
        mGroupsOptional = options.optionalGroups.stream().map(SearchGroup::new).toList();
        mGroupsAll =
                Stream.of(mGroupsRequired, mGroupsOptional).flatMap(Collection::stream).toList();
    }

    /** Main entry point for {@link ContentCaptureEvent} processing. */
@@ -130,9 +123,31 @@ public class ContentProtectionEventProcessor {

    @UiThread
    private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) {
        mPasswordFieldDetected |= isPasswordField(event);
        mSuspiciousTextDetected |= isSuspiciousText(event);
        if (mPasswordFieldDetected && mSuspiciousTextDetected) {
        ViewNode viewNode = event.getViewNode();
        String eventText = ContentProtectionUtils.getEventTextLower(event);
        String viewNodeText = ContentProtectionUtils.getViewNodeTextLower(viewNode);
        String hintText = ContentProtectionUtils.getHintTextLower(viewNode);

        mGroupsAll.stream()
                .filter(group -> !group.mFound)
                .filter(
                        group ->
                                group.matches(eventText)
                                        || group.matches(viewNodeText)
                                        || group.matches(hintText))
                .findFirst()
                .ifPresent(
                        group -> {
                            group.mFound = true;
                            mAnyGroupFound = true;
                        });

        boolean loginDetected =
                mGroupsRequired.stream().allMatch(group -> group.mFound)
                        && mGroupsOptional.stream().filter(group -> group.mFound).count()
                                >= mOptions.optionalGroupsThreshold;

        if (loginDetected) {
            loginDetected();
        } else {
            maybeResetLoginFlags();
@@ -150,14 +165,13 @@ public class ContentProtectionEventProcessor {

    @UiThread
    private void resetLoginFlags() {
        mPasswordFieldDetected = false;
        mSuspiciousTextDetected = false;
        mResetLoginRemainingEventsToProcess = 0;
        mGroupsAll.forEach(group -> group.mFound = false);
        mAnyGroupFound = false;
    }

    @UiThread
    private void maybeResetLoginFlags() {
        if (mPasswordFieldDetected || mSuspiciousTextDetected) {
        if (mAnyGroupFound) {
            if (mResetLoginRemainingEventsToProcess <= 0) {
                mResetLoginRemainingEventsToProcess = RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS;
            } else {
@@ -194,61 +208,21 @@ public class ContentProtectionEventProcessor {
        }
    }

    private boolean isPasswordField(@NonNull ContentCaptureEvent event) {
        return isPasswordField(event.getViewNode());
    }
    private static final class SearchGroup {

    private boolean isPasswordField(@Nullable ViewNode viewNode) {
        if (viewNode == null) {
            return false;
        }
        return isAndroidPasswordField(viewNode) || isWebViewPasswordField(viewNode);
    }
        @NonNull private final List<String> mSearchStrings;

    private boolean isAndroidPasswordField(@NonNull ViewNode viewNode) {
        if (!isAndroidViewNode(viewNode)) {
            return false;
        }
        int inputType = viewNode.getInputType();
        return PASSWORD_FIELD_INPUT_TYPES.stream()
                .anyMatch(passwordInputType -> (inputType & passwordInputType) != 0);
    }

    private boolean isWebViewPasswordField(@NonNull ViewNode viewNode) {
        if (viewNode.getClassName() != null) {
            return false;
        }
        return isPasswordText(ContentProtectionUtils.getViewNodeText(viewNode));
    }
        public boolean mFound = false;

    private boolean isAndroidViewNode(@NonNull ViewNode viewNode) {
        String className = viewNode.getClassName();
        return className != null && className.startsWith(ANDROID_CLASS_NAME_PREFIX);
        SearchGroup(@NonNull List<String> searchStrings) {
            mSearchStrings = searchStrings;
        }

    private boolean isSuspiciousText(@NonNull ContentCaptureEvent event) {
        return isSuspiciousText(ContentProtectionUtils.getEventText(event))
                || isSuspiciousText(ContentProtectionUtils.getViewNodeText(event));
    }

    private boolean isSuspiciousText(@Nullable String text) {
        public boolean matches(@Nullable String text) {
            if (text == null) {
                return false;
            }
        if (isPasswordText(text)) {
            return true;
        }
        String lowerCaseText = text.toLowerCase();
        return ADDITIONAL_SUSPICIOUS_TEXTS.stream()
                .anyMatch(suspiciousText -> lowerCaseText.contains(suspiciousText));
    }

    private boolean isPasswordText(@Nullable String text) {
        if (text == null) {
            return false;
            return mSearchStrings.stream().anyMatch(text::contains);
        }
        String lowerCaseText = text.toLowerCase();
        return PASSWORD_TEXTS.stream()
                .anyMatch(passwordText -> lowerCaseText.contains(passwordText));
    }
}
+17 −11
Original line number Diff line number Diff line
@@ -28,33 +28,39 @@ import android.view.contentcapture.ViewNode;
 */
public final class ContentProtectionUtils {

    /** Returns the text extracted directly from the {@link ContentCaptureEvent}, if set. */
    /** Returns the lowercase text extracted from the {@link ContentCaptureEvent}, if set. */
    @Nullable
    public static String getEventText(@NonNull ContentCaptureEvent event) {
    public static String getEventTextLower(@NonNull ContentCaptureEvent event) {
        CharSequence text = event.getText();
        if (text == null) {
            return null;
        }
        return text.toString();
        return text.toString().toLowerCase();
    }

    /** Returns the text extracted from the event's {@link ViewNode}, if set. */
    /** Returns the lowercase text extracted from the {@link ViewNode}, if set. */
    @Nullable
    public static String getViewNodeText(@NonNull ContentCaptureEvent event) {
        ViewNode viewNode = event.getViewNode();
    public static String getViewNodeTextLower(@Nullable ViewNode viewNode) {
        if (viewNode == null) {
            return null;
        }
        return getViewNodeText(viewNode);
        CharSequence text = viewNode.getText();
        if (text == null) {
            return null;
        }
        return text.toString().toLowerCase();
    }

    /** Returns the text extracted directly from the {@link ViewNode}, if set. */
    /** Returns the lowercase hint text extracted from the {@link ViewNode}, if set. */
    @Nullable
    public static String getViewNodeText(@NonNull ViewNode viewNode) {
        CharSequence text = viewNode.getText();
    public static String getHintTextLower(@Nullable ViewNode viewNode) {
        if (viewNode == null) {
            return null;
        }
        String text = viewNode.getHint();
        if (text == null) {
            return null;
        }
        return text.toString();
        return text.toLowerCase();
    }
}
+21 −1
Original line number Diff line number Diff line
@@ -115,6 +115,26 @@ public class MainContentCaptureSessionTest {
                        new ContentCaptureOptions.ContentProtectionOptions(
                                /* enableReceiver= */ true,
                                -BUFFER_SIZE,
                                /* requiredGroups= */ List.of(List.of("a")),
                                /* optionalGroups= */ Collections.emptyList(),
                                /* optionalGroupsThreshold= */ 0));
        MainContentCaptureSession session = createSession(options);
        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;

        session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);

        assertThat(session.mContentProtectionEventProcessor).isNull();
        verifyZeroInteractions(mMockContentProtectionEventProcessor);
    }

    @Test
    public void onSessionStarted_contentProtectionNoGroups_processorNotCreated() {
        ContentCaptureOptions options =
                createOptions(
                        /* enableContentCaptureReceiver= */ true,
                        new ContentCaptureOptions.ContentProtectionOptions(
                                /* enableReceiver= */ true,
                                BUFFER_SIZE,
                                /* requiredGroups= */ Collections.emptyList(),
                                /* optionalGroups= */ Collections.emptyList(),
                                /* optionalGroupsThreshold= */ 0));
@@ -320,7 +340,7 @@ public class MainContentCaptureSessionTest {
                new ContentCaptureOptions.ContentProtectionOptions(
                        enableContentProtectionReceiver,
                        BUFFER_SIZE,
                        /* requiredGroups= */ Collections.emptyList(),
                        /* requiredGroups= */ List.of(List.of("a")),
                        /* optionalGroups= */ Collections.emptyList(),
                        /* optionalGroupsThreshold= */ 0));
    }
+197 −299

File changed.

Preview size limit exceeded, changes collapsed.

Loading