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

Commit 357f2d85 authored by Haining Chen's avatar Haining Chen
Browse files

Add adaptive authentication service

Adaptive auth aims to provide more secure and better authentication
experience using various signals. In its current version, it detects
repeated failed auth attempts using either primary auth methods (i.e.
PIN/pattern/password) or biometrics, and then decides whether to lock
out the device.

Flag: ACONFIG android.adaptiveauth.enable_adaptive_auth DEVELOPMENT
Bug: 285053096
Test: atest AdaptiveAuthServiceTest
Change-Id: If15115e469f60d20f590c6fb0fde62a75c771b8e
parent bde6eeb6
Loading
Loading
Loading
Loading
+16 −3
Original line number Diff line number Diff line
@@ -1615,7 +1615,8 @@ public class LockPatternUtils {
                        STRONG_AUTH_REQUIRED_AFTER_TIMEOUT,
                        STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
                        STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT,
                        SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED})
                        SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED,
                        SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface StrongAuthFlags {}

@@ -1641,7 +1642,8 @@ public class LockPatternUtils {

        /**
         * Strong authentication is required because the user has been locked out after too many
         * attempts.
         * attempts using primary auth methods (i.e. PIN/pattern/password) from the lock screen,
         * Android Settings, and BiometricPrompt where user authentication is required.
         */
        public static final int STRONG_AUTH_REQUIRED_AFTER_LOCKOUT = 0x8;

@@ -1673,13 +1675,24 @@ public class LockPatternUtils {
         */
        public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100;

        /**
         * Some authentication is required because adaptive auth has requested to lock device due to
         * repeated failed primary auth (i.e. PIN/pattern/password) or biometric auth attempts which
         * can come from Android Settings or BiometricPrompt where user authentication is required,
         * in addition to from the lock screen. When a risk is determined, adaptive auth will
         * proactively prompt the lock screen and will require users to re-enter the device with
         * either primary auth or biometric auth (if not prohibited by other flags).
         */
        public static final int SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST = 0x200;

        /**
         * Strong auth flags that do not prevent biometric methods from being accepted as auth.
         * If any other flags are set, biometric authentication is disabled.
         */
        private static final int ALLOWING_BIOMETRIC = STRONG_AUTH_NOT_REQUIRED
                | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST
                | SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
                | SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
                | SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;

        private final SparseIntArray mStrongAuthRequiredForUser = new SparseIntArray();
        private final H mHandler;
+238 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.adaptiveauth;

import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;

import android.app.KeyguardManager;
import android.content.Context;
import android.hardware.biometrics.AuthenticationStateListener;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricSourceType;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import android.util.Slog;
import android.util.SparseIntArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockSettingsInternal;
import com.android.internal.widget.LockSettingsStateListener;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.WindowManagerInternal;

import java.util.Objects;

/**
 * @hide
 */
public class AdaptiveAuthService extends SystemService {
    private static final String TAG = "AdaptiveAuthService";
    private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);

    @VisibleForTesting
    static final int MAX_ALLOWED_FAILED_AUTH_ATTEMPTS = 5;
    private static final int MSG_REPORT_PRIMARY_AUTH_ATTEMPT = 1;
    private static final int MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT = 2;
    private static final int AUTH_SUCCESS = 1;
    private static final int AUTH_FAILURE = 0;

    private final LockPatternUtils mLockPatternUtils;
    private final LockSettingsInternal mLockSettings;
    private final BiometricManager mBiometricManager;
    private final KeyguardManager mKeyguardManager;
    private final PowerManager mPowerManager;
    private final WindowManagerInternal mWindowManager;
    private final UserManagerInternal mUserManager;
    @VisibleForTesting
    final SparseIntArray mFailedAttemptsForUser = new SparseIntArray();

    public AdaptiveAuthService(Context context) {
        this(context, new LockPatternUtils(context));
    }

    @VisibleForTesting
    public AdaptiveAuthService(Context context, LockPatternUtils lockPatternUtils) {
        super(context);
        mLockPatternUtils = lockPatternUtils;
        mLockSettings = Objects.requireNonNull(
                LocalServices.getService(LockSettingsInternal.class));
        mBiometricManager = Objects.requireNonNull(
                context.getSystemService(BiometricManager.class));
        mKeyguardManager = Objects.requireNonNull(context.getSystemService(KeyguardManager.class));
        mPowerManager = Objects.requireNonNull(context.getSystemService(PowerManager.class));
        mWindowManager = Objects.requireNonNull(
                LocalServices.getService(WindowManagerInternal.class));
        mUserManager = Objects.requireNonNull(LocalServices.getService(UserManagerInternal.class));
    }

    @Override
    public void onStart() {}

    @Override
    public void onBootPhase(int phase) {
        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
            init();
        }
    }

    @VisibleForTesting
    void init() {
        mLockSettings.registerLockSettingsStateListener(mLockSettingsStateListener);
        mBiometricManager.registerAuthenticationStateListener(mAuthenticationStateListener);
    }

    private final LockSettingsStateListener mLockSettingsStateListener =
            new LockSettingsStateListener() {
                @Override
                public void onAuthenticationSucceeded(int userId) {
                    if (DEBUG) {
                        Slog.d(TAG, "LockSettingsStateListener#onAuthenticationSucceeded");
                    }
                    mHandler.obtainMessage(MSG_REPORT_PRIMARY_AUTH_ATTEMPT, AUTH_SUCCESS, userId)
                            .sendToTarget();
                }

                @Override
                public void onAuthenticationFailed(int userId) {
                    Slog.i(TAG, "LockSettingsStateListener#onAuthenticationFailed");
                    mHandler.obtainMessage(MSG_REPORT_PRIMARY_AUTH_ATTEMPT, AUTH_FAILURE, userId)
                            .sendToTarget();
                }
            };

    private final AuthenticationStateListener mAuthenticationStateListener =
            new AuthenticationStateListener.Stub() {
                @Override
                public void onAuthenticationStarted(int requestReason) {}

                @Override
                public void onAuthenticationStopped() {}

                @Override
                public void onAuthenticationSucceeded(int requestReason, int userId) {
                    if (DEBUG) {
                        Slog.d(TAG, "AuthenticationStateListener#onAuthenticationSucceeded");
                    }
                    mHandler.obtainMessage(MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT, AUTH_SUCCESS, userId)
                            .sendToTarget();
                }

                @Override
                public void onAuthenticationFailed(int requestReason, int userId) {
                    Slog.i(TAG, "AuthenticationStateListener#onAuthenticationFailed");
                    mHandler.obtainMessage(MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT, AUTH_FAILURE, userId)
                            .sendToTarget();
                }

                @Override
                public void onAuthenticationAcquired(BiometricSourceType biometricSourceType,
                        int requestReason, int acquiredInfo) {}
            };

    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_REPORT_PRIMARY_AUTH_ATTEMPT:
                    handleReportPrimaryAuthAttempt(msg.arg1 != AUTH_FAILURE, msg.arg2);
                    break;
                case MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT:
                    handleReportBiometricAuthAttempt(msg.arg1 != AUTH_FAILURE, msg.arg2);
                    break;
            }
        }
    };

    private void handleReportPrimaryAuthAttempt(boolean success, int userId) {
        if (DEBUG) {
            Slog.d(TAG, "handleReportPrimaryAuthAttempt: success=" + success
                    + ", userId=" + userId);
        }
        reportAuthAttempt(success, userId);
    }

    private void handleReportBiometricAuthAttempt(boolean success, int userId) {
        if (DEBUG) {
            Slog.d(TAG, "handleReportBiometricAuthAttempt: success=" + success
                    + ", userId=" + userId);
        }
        reportAuthAttempt(success, userId);
    }

    private void reportAuthAttempt(boolean success, int userId) {
        if (success) {
            // Deleting the entry effectively resets the counter of failed attempts for the user
            mFailedAttemptsForUser.delete(userId);
            return;
        }

        final int numFailedAttempts = mFailedAttemptsForUser.get(userId, 0) + 1;
        Slog.i(TAG, "reportAuthAttempt: numFailedAttempts=" + numFailedAttempts
                + ", userId=" + userId);
        mFailedAttemptsForUser.put(userId, numFailedAttempts);

        // Don't lock again if the device is already locked and if Keyguard is already showing and
        // isn't trivially dismissible
        if (mKeyguardManager.isDeviceLocked(userId) && mKeyguardManager.isKeyguardLocked()) {
            Slog.d(TAG, "Not locking the device because the device is already locked.");
            return;
        }

        if (numFailedAttempts < MAX_ALLOWED_FAILED_AUTH_ATTEMPTS) {
            Slog.d(TAG, "Not locking the device because the number of failed attempts is below"
                    + " the threshold.");
            return;
        }

        //TODO: additionally consider the trust signal before locking device
        lockDevice(userId);
    }

    /**
     * Locks the device and requires primary auth or biometric auth for unlocking
     */
    private void lockDevice(int userId) {
        // Require either primary auth or biometric auth to unlock the device again. Keyguard and
        // bouncer will also check the StrongAuthFlag for the user to display correct strings for
        // explaining why the device is locked
        mLockPatternUtils.requireStrongAuth(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST, userId);

        // If userId is a profile that has a different parent userId (regardless of its profile
        // type, or whether it's a profile with unified challenges or not), its parent userId that
        // owns the Keyguard will also be locked
        final int parentUserId = mUserManager.getProfileParentId(userId);
        Slog.i(TAG, "lockDevice: userId=" + userId + ", parentUserId=" + parentUserId);
        if (parentUserId != userId) {
            mLockPatternUtils.requireStrongAuth(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST,
                    parentUserId);
        }

        // Power off the display
        mPowerManager.goToSleep(SystemClock.uptimeMillis());

        // Lock the device
        mWindowManager.lockNow();
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -107,6 +107,7 @@ import com.android.internal.util.EmergencyAffordanceManager;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.widget.ILockSettings;
import com.android.internal.widget.LockSettingsInternal;
import com.android.server.adaptiveauth.AdaptiveAuthService;
import com.android.server.am.ActivityManagerService;
import com.android.server.appbinding.AppBindingService;
import com.android.server.appop.AppOpMigrationHelper;
@@ -2615,6 +2616,12 @@ public final class SystemServer implements Dumpable {
            mSystemServiceManager.startService(AuthService.class);
            t.traceEnd();

            if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
                t.traceBegin("StartAdaptiveAuthService");
                mSystemServiceManager.startService(AdaptiveAuthService.class);
                t.traceEnd();
            }

            if (!isWatch) {
                // We don't run this on watches as there are no plans to use the data logged
                // on watch devices.
+333 −0

File added.

Preview size limit exceeded, changes collapsed.

+1 −0
Original line number Diff line number Diff line
include /services/core/java/com/android/server/adaptiveauth/OWNERS
 No newline at end of file