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

Commit 60f0cc7b authored by vaibsinghal's avatar vaibsinghal Committed by Vaibhav Singhal
Browse files

Add AppFuntionAllow List Updation.

Flag: android.permission.flags.app_function_access_service_enabled
Test: atest CtsAppFunctionTestCases and FrameworksAppFunctionsTests
Bug: 416661798
Change-Id: I91eab738f9a8cd13031ac3feae7a7214c0ecc532
parent c8ba2fa1
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.app.appfunctions.AppFunctionManagerConfiguration;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.permission.flags.Flags;

import com.android.server.LocalServices;
import com.android.server.SystemService;
@@ -42,6 +43,13 @@ public class AppFunctionManagerService extends SystemService {
        }
    }

    @Override
    public void onBootPhase(int phase) {
        if (Flags.appFunctionAccessServiceEnabled()) {
            mServiceImpl.onBootPhase(phase);
        }
    }

    @Override
    public void onUserUnlocked(@NonNull TargetUser user) {
        mServiceImpl.onUserUnlocked(user);
+69 −1
Original line number Diff line number Diff line
@@ -56,6 +56,7 @@ import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SignedPackage;
import android.content.pm.SigningInfo;
import android.os.Binder;
import android.os.CancellationSignal;
@@ -68,6 +69,8 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.permission.flags.Flags;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.Slog;
import android.util.SparseArray;
@@ -76,12 +79,15 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.DumpUtils;
import com.android.server.SystemService;
import com.android.server.SystemService.TargetUser;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
@@ -91,6 +97,9 @@ import java.util.concurrent.Executor;
/** Implementation of the AppFunctionManagerService. */
public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
    private static final String TAG = AppFunctionManagerServiceImpl.class.getSimpleName();
    private static final String ALLOWLISTED_APP_FUNCTIONS_AGENTS =
            "allowlisted_app_functions_agents";
    private static final String NAMESPACE_MACHINE_LEARNING = "machine_learning";

    private final RemoteServiceCaller<IAppFunctionService> mRemoteServiceCaller;
    private final CallerValidator mCallerValidator;
@@ -137,7 +146,6 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
    /** Called when the user is unlocked. */
    public void onUserUnlocked(TargetUser user) {
        Objects.requireNonNull(user);

        registerAppSearchObserver(user);
        trySyncRuntimeMetadata(user);
        PackageMonitor pkgMonitorForUser =
@@ -184,6 +192,66 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
                .exec(this, in, out, err, args, callback, resultReceiver);
    }

    private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigListener =
            new DeviceConfig.OnPropertiesChangedListener() {

                @Override
                public void onPropertiesChanged(DeviceConfig.Properties properties) {
                    if (Flags.appFunctionAccessServiceEnabled()) {
                        if (properties.getKeyset().contains(ALLOWLISTED_APP_FUNCTIONS_AGENTS)) {
                            final String signaturesString =
                                    properties.getString(ALLOWLISTED_APP_FUNCTIONS_AGENTS, "");
                            Slog.d(TAG, "onPropertiesChanged signatureString " + signaturesString);
                            try {
                                final List<SignedPackage> allowedSignedPackages =
                                        SignedPackageParser.parseList(signaturesString);
                                // TODO(b/416661798): Calls new
                                // AppFunctionAccessService#updateAgentAllowlist API to update
                                // the allowlist
                            } catch (Exception e) {
                                Slog.e(
                                        TAG,
                                        "Cannot parse signature string: " + signaturesString,
                                        e);
                            }
                        }
                    }
                }
            };

    /**
     * Called during different phases of the system boot process.
     *
     * <p>This method is used to initialize AppFunctionManagerService components that depend on
     * other system services being ready. Specifically, it handles reading DeviceConfig properties
     * related to allowed agent package signatures and registers a listener for changes to these
     * properties.
     *
     * @param phase The current boot phase, as defined in {@link SystemService}. This method
     *     specifically acts on {@link SystemService#PHASE_SYSTEM_SERVICES_READY}.
     */
    public void onBootPhase(int phase) {
        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
            final String signatureString =
                    DeviceConfig.getString(
                            NAMESPACE_MACHINE_LEARNING, ALLOWLISTED_APP_FUNCTIONS_AGENTS, "");
            try {
                final List<SignedPackage> allowedSignedPackages =
                        SignedPackageParser.parseList(signatureString);

                // TODO(b/416661798): Similar to the callback, update the allowlist with
                // AppFunctionAccessService#updateAgentAllowlist API.

            } catch (Exception e) {
                Slog.e(TAG, "Cannot parse signature string: " + signatureString, e);
            }
            DeviceConfig.addOnPropertiesChangedListener(
                    NAMESPACE_MACHINE_LEARNING,
                    BackgroundThread.getExecutor(),
                    mDeviceConfigListener);
        }
    }

    @Override
    public ICancellationSignal executeAppFunction(
            @NonNull ExecuteAppFunctionAidlRequest requestInternal,
+84 −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.appfunctions;

import android.annotation.NonNull;
import android.content.pm.Signature;
import android.content.pm.SignedPackage;
import android.util.Slog;

import java.util.ArrayList;
import java.util.List;

public class SignedPackageParser {
    private static final String SIGNED_PACKAGE_SEPARATOR = ";";
    private static final String CERTIFICATE_SEPARATOR = ":";
    private static final String TAG = SignedPackageParser.class.getSimpleName();

    /**
     * Parse a {@link SignedPackage} from a string input.
     *
     * @param input the package name, optionally followed by a colon and a signing certificate
     *     digest
     * @return the parsed {@link SignedPackage}, or {@code null} if the input is invalid
     */
    private static SignedPackage parse(@NonNull String input) {
        String packageName;
        byte[] certificate;
        int certificateSeparatorIndex = input.indexOf(CERTIFICATE_SEPARATOR);
        if (certificateSeparatorIndex != -1) {
            packageName = input.substring(0, certificateSeparatorIndex);
            String certificateString = input.substring(certificateSeparatorIndex + 1);
            try {
                certificate = new Signature(certificateString).toByteArray();
            } catch (IllegalArgumentException e) {
                Slog.w(TAG, "Cannot parse the signed package input of: " + input, e);
                return null;
            }
        } else {
            packageName = input;
            certificate = null;
        }
        return new SignedPackage(packageName, certificate);
    }

    /**
     * Parse a list of {@link SignedPackage}s from a string input.
     *
     * @param input the package names, each optionally followed by a colon and a signing certificate
     *     digest
     * @return the parsed list of valid {@link SignedPackage}s
     */
    @NonNull
    public static List<SignedPackage> parseList(@NonNull String input) throws Exception {
        List<SignedPackage> signedPackages = new ArrayList<>();
        if (input.isEmpty()) {
            return signedPackages;
        }

        for (String signedPackageInput : input.split(SIGNED_PACKAGE_SEPARATOR)) {
            SignedPackage signedPackage = parse(signedPackageInput);
            if (signedPackage == null) {
                throw new IllegalArgumentException(
                        "Cannot parse the signed package input of: " + signedPackageInput);
            } else {
                signedPackages.add(signedPackage);
            }
        }
        return signedPackages;
    }
}
+127 −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.appfunctions

import android.content.pm.Signature
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class SignedPackageParserTest {

    @Test
    fun parseList_emptyInput_returnsEmptyList() {
        val input = ""

        val result = SignedPackageParser.parseList(input)

        assertThat(result).isEmpty()
    }

    @Test
    fun parseList_singlePackageName_returnsCorrectList() {
        val input = TEST_PACKAGE_NAME_1

        val result = SignedPackageParser.parseList(input)

        assertThat(result).hasSize(1)
        assertThat(result[0].packageName).isEqualTo(TEST_PACKAGE_NAME_1)
        assertThat(result[0].certificateDigest).isNull()
    }

    @Test
    fun parseList_singlePackageNameWithCertificate_returnsCorrectList() {
        val input = "$TEST_PACKAGE_NAME_1:$TEST_CERTIFICATE_STRING_1"

        val result = SignedPackageParser.parseList(input)

        assertThat(result).hasSize(1)
        assertThat(result[0].packageName).isEqualTo(TEST_PACKAGE_NAME_1)
        assertThat(result[0].certificateDigest).isEqualTo(TEST_CERTIFICATE_DIGEST_1)
    }

    @Test
    fun parseList_multiplePackages_returnsCorrectList() {
        val input = "$TEST_PACKAGE_NAME_1:$TEST_CERTIFICATE_STRING_1;$TEST_PACKAGE_NAME_2"

        val result = SignedPackageParser.parseList(input)

        assertThat(result).hasSize(2)
        assertThat(result[0].packageName).isEqualTo(TEST_PACKAGE_NAME_1)
        assertThat(result[0].certificateDigest).isEqualTo(TEST_CERTIFICATE_DIGEST_1)
        assertThat(result[1].packageName).isEqualTo(TEST_PACKAGE_NAME_2)
        assertThat(result[1].certificateDigest).isNull()
    }

    @Test
    fun parseList_multiplePackagesWithCertificates_returnsCorrectList() {
        val input =
            "$TEST_PACKAGE_NAME_1:$TEST_CERTIFICATE_STRING_1;" +
                "$TEST_PACKAGE_NAME_2:$TEST_CERTIFICATE_STRING_2"

        val result = SignedPackageParser.parseList(input)

        assertThat(result).hasSize(2)
        assertThat(result[0].packageName).isEqualTo(TEST_PACKAGE_NAME_1)
        assertThat(result[0].certificateDigest).isEqualTo(TEST_CERTIFICATE_DIGEST_1)
        assertThat(result[1].packageName).isEqualTo(TEST_PACKAGE_NAME_2)
        assertThat(result[1].certificateDigest).isEqualTo(TEST_CERTIFICATE_DIGEST_2)
    }

    @Test
    fun parseList_invalidCertificate_throwsException() {
        val input =
            "$TEST_PACKAGE_NAME_1:$INVALID_CERTIFICATE_STRING;" +
                "$TEST_PACKAGE_NAME_2:$TEST_CERTIFICATE_STRING_2"

        assertFailsWith<IllegalArgumentException> { SignedPackageParser.parseList(input) }
    }

    @Test
    fun parseList_trailingSeparator_handlesPotentialEmptyLastSegment() {
        val input = "$TEST_PACKAGE_NAME_1:$TEST_CERTIFICATE_STRING_1;"

        val result = SignedPackageParser.parseList(input)

        assertThat(result).hasSize(1)
        assertThat(result[0].packageName).isEqualTo(TEST_PACKAGE_NAME_1)
        assertThat(result[0].certificateDigest).isEqualTo(TEST_CERTIFICATE_DIGEST_1)
    }

    @Test
    fun parseList_onlySeparators_returnsEmptyList() {
        val input = ";;"

        val result = SignedPackageParser.parseList(input)

        assertThat(result).isEmpty()
    }

    private companion object {
        const val TEST_PACKAGE_NAME_1 = "com.example.test1"
        const val TEST_PACKAGE_NAME_2 = "com.example.test2"
        const val TEST_CERTIFICATE_STRING_1 = "abcdef0123456789"
        val TEST_CERTIFICATE_DIGEST_1 = Signature(TEST_CERTIFICATE_STRING_1).toByteArray()
        const val TEST_CERTIFICATE_STRING_2 = "9876543210fedcba"
        val TEST_CERTIFICATE_DIGEST_2 = Signature(TEST_CERTIFICATE_STRING_2).toByteArray()

        const val INVALID_CERTIFICATE_STRING = "invalid_certificate_string"
    }
}