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

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

Add storage for the App Function Agent allowlist.

Introduces `AppFunctionAgentAllowlistStorage` to persist the agent allowlist from DeviceConfig to a file. This provides a last-known-good configuration that can be used as a fallback.
Flag: EXEMPT Class not used anywhere
Test: atest FrameworksAppFunctionsTests
Bug: 416661798

Change-Id: Iba401dd246702d9ea36474a5d4b084a7dc06114f
parent 2583ac54
Loading
Loading
Loading
Loading
+97 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.annotation.Nullable;
import android.annotation.WorkerThread;
import android.content.pm.SignedPackage;
import android.util.AtomicFile;
import android.util.Slog;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

/** Handles reading and writing the App Function agent allowlist to persistent storage. */
public final class AppFunctionAgentAllowlistStorage {

    private static final String TAG = "AppFunctionAgentAllowlistStorage";

    @NonNull private final AtomicFile mAtomicFile;

    /**
     * Creates an instance that manages the allowlist at the specified file path.
     *
     * @param file The file to read from and write to.
     */
    public AppFunctionAgentAllowlistStorage(@NonNull File file) {
        mAtomicFile = new AtomicFile(file);
    }

    /**
     * Reads and parses the allowlist from persistent storage.
     *
     * <p>This is a blocking operation and should be called on a worker thread.
     *
     * <p>If the file is found to be corrupt during reading or parsing, it will be deleted.
     *
     * @return A list of {@link SignedPackage}s if the file exists and is valid, or {@code null}
     */
    @WorkerThread
    @Nullable
    public List<SignedPackage> readPreviousValidAllowlist() {
        if (!mAtomicFile.exists()) {
            Slog.d(TAG, "Allowlist file does not exist.");
            return null;
        }

        try {
            byte[] data = mAtomicFile.readFully();
            String allowlistString = new String(data, StandardCharsets.UTF_8);
            return SignedPackageParser.parseList(allowlistString);
        } catch (Exception e) {
            Slog.e(TAG, "Error reading or parsing allowlist file", e);
            mAtomicFile.delete();
            return null;
        }
    }

    /**
     * Writes the given allowlist string to persistent storage atomically.
     *
     * <p>This is a blocking operation and should be called on a worker thread.
     *
     * @param allowlistString The raw string representation of the allowlist to be written.
     */
    @WorkerThread
    public void writeCurrentAllowlist(@NonNull String allowlistString) {
        FileOutputStream fos = null;
        try {
            fos = mAtomicFile.startWrite();
            fos.write(allowlistString.getBytes(StandardCharsets.UTF_8));
            mAtomicFile.finishWrite(fos);
        } catch (IOException e) {
            Slog.e(TAG, "Error writing allowlist file", e);
            if (fos != null) {
                mAtomicFile.failWrite(fos);
            }
        }
    }
}
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.Context
import android.content.pm.Signature
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.nio.charset.StandardCharsets
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class AppFunctionAgentAllowlistStorageTest {

    private lateinit var context: Context
    private lateinit var testDir: File
    private lateinit var storage: AppFunctionAgentAllowlistStorage
    private lateinit var allowlistFile: File

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
        // Use a temporary directory for testing
        testDir = File(context.cacheDir, "appfunctions_test")
        testDir.mkdirs()
        val allowlistDir = File(testDir, "system/appfunctions")
        allowlistDir.mkdirs() // Ensure the parent directory exists
        allowlistFile = File(allowlistDir, "agent_allowlist.txt")
        storage = AppFunctionAgentAllowlistStorage(allowlistFile)
    }

    @After
    fun tearDown() {
        testDir.deleteRecursively()
    }

    @Test
    fun readPreviousValidAllowlist_fileDoesNotExist_returnsNull() {
        val allowlist = storage.readPreviousValidAllowlist()
        assertThat(allowlist).isNull()
    }

    @Test
    fun writeCurrentValidAllowlist_readsBackCorrectly() {
        val allowlistString =
            "$TEST_PACKAGE_NAME_1:$TEST_CERTIFICATE_STRING_1;" +
                "$TEST_PACKAGE_NAME_2:$TEST_CERTIFICATE_STRING_2"
        storage.writeCurrentAllowlist(allowlistString)

        val readAllowlist = storage.readPreviousValidAllowlist()

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

    @Test
    fun readPreviousValidAllowlist_invalidFileContent_returnsNullAndDeletesFile() {
        val invalidAllowlistString = "$TEST_PACKAGE_NAME_1:$INVALID_CERTIFICATE_STRING"

        storage.writeCurrentAllowlist(invalidAllowlistString)
        assertThat(allowlistFile.exists()).isTrue()

        val allowlist = storage.readPreviousValidAllowlist()

        assertThat(allowlist).isNull()
        assertThat(allowlistFile.exists()).isFalse()
    }

    @Test
    fun writeCurrentValidAllowlist_emptyString_createsEmptyFile() {
        val allowlistString = ""
        storage.writeCurrentAllowlist(allowlistString)

        val readAllowlist = storage.readPreviousValidAllowlist()

        assertThat(readAllowlist).isNotNull()
        assertThat(readAllowlist).isEmpty()
        assertThat(allowlistFile.exists()).isTrue()
        assertThat(allowlistFile.readText(StandardCharsets.UTF_8)).isEmpty()
    }

    @Test
    fun writeCurrentAllowlist_writeTwice_readsBackCorrectlyTheSecondValue() {
        val allowlistString1 = "$TEST_PACKAGE_NAME_1:$TEST_CERTIFICATE_STRING_1;"
        storage.writeCurrentAllowlist(allowlistString1)
        val allowlistString2 = "$TEST_PACKAGE_NAME_2:$TEST_CERTIFICATE_STRING_2"
        storage.writeCurrentAllowlist(allowlistString2)

        val readAllowlist = storage.readPreviousValidAllowlist()

        assertThat(readAllowlist).isNotNull()
        assertThat(readAllowlist).hasSize(1)
        assertThat(readAllowlist!![0].packageName).isEqualTo(TEST_PACKAGE_NAME_2)
        assertThat(readAllowlist[0].certificateDigest).isEqualTo(TEST_CERTIFICATE_DIGEST_2)
    }

    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"
    }
}