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

Commit 54708ada authored by Wenhao Wang's avatar Wenhao Wang
Browse files

[Forensic] Add DataAggregator

The DataAggregator is in charge of aggregating the data from
the list of DataSources.
Whenever new data needs to be tranferred from DataSource,
the data go through DataAggregator and ForensicService.
The DataAggregator can also be used to enable and disable
the data collection in the list of DataSources.

Bug: 365994454
Test: atest ForensicServiceTest
Flag: android.security.afl_api
Ignore-AOSP-First: security feature
Change-Id: I5363dff2b2ca0b5b1631ba431a858b5c6ec9f786
parent f96d78f4
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -61,6 +61,14 @@ public final class ForensicEvent implements Parcelable {
        in.readMap(mKeyValuePairs, getClass().getClassLoader(), String.class, String.class);
    }

    public String getType() {
        return mType;
    }

    public Map<String, String> getKeyValuePairs() {
        return mKeyValuePairs;
    }

    @Override
    public void writeToParcel(@NonNull Parcel out, int flags) {
        out.writeString(mType);
+142 −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.security.forensic;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.security.forensic.ForensicEvent;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceThread;

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

public class DataAggregator {
    private static final String TAG = "Forensic DataAggregator";
    private static final int MSG_SINGLE_DATA = 0;
    private static final int MSG_BATCH_DATA = 1;
    private static final int MSG_DISABLE = 2;

    private static final int STORED_EVENTS_SIZE_LIMIT = 1024;
    private final ForensicService mForensicService;
    private final ArrayList<DataSource> mDataSources;

    private List<ForensicEvent> mStoredEvents = new ArrayList<>();
    private ServiceThread mHandlerThread;
    private Handler mHandler;
    public DataAggregator(ForensicService forensicService) {
        mForensicService = forensicService;
        mDataSources = new ArrayList<DataSource>();
    }

    @VisibleForTesting
    void setHandler(Looper looper, ServiceThread serviceThread) {
        mHandlerThread = serviceThread;
        mHandler = new EventHandler(looper, this);
    }

    /**
     * Initialize DataSources
     * @return Whether the initialization succeeds.
     */
    // TODO: Add the corresponding data sources
    public boolean initialize() {
        return true;
    }

    /**
     * Enable the data collection of all DataSources.
     */
    public void enable() {
        mHandlerThread = new ServiceThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND,
                /* allowIo */ false);
        mHandlerThread.start();
        mHandler = new EventHandler(mHandlerThread.getLooper(), this);
        for (DataSource ds : mDataSources) {
            ds.enable();
        }
    }

    /**
     * DataSource calls it to transmit a single event.
     */
    public void addSingleData(ForensicEvent event) {
        mHandler.obtainMessage(MSG_SINGLE_DATA, event).sendToTarget();
    }

    /**
     * DataSource calls it to transmit list of events.
     */
    public void addBatchData(List<ForensicEvent> events) {
        mHandler.obtainMessage(MSG_BATCH_DATA, events).sendToTarget();
    }

    /**
     * Disable the data collection of all DataSources.
     */
    public void disable() {
        mHandler.obtainMessage(MSG_DISABLE).sendToTarget();
    }

    private void onNewSingleData(ForensicEvent event) {
        if (mStoredEvents.size() < STORED_EVENTS_SIZE_LIMIT) {
            mStoredEvents.add(event);
        } else {
            mForensicService.addNewData(mStoredEvents);
            mStoredEvents = new ArrayList<>();
        }
    }

    private void onNewBatchData(List<ForensicEvent> events) {
        mForensicService.addNewData(events);
    }

    private void onDisable() {
        for (DataSource ds : mDataSources) {
            ds.disable();
        }
        mHandlerThread.quitSafely();
        mHandlerThread = null;
    }

    private static class EventHandler extends Handler {
        private final DataAggregator mDataAggregator;
        EventHandler(Looper looper, DataAggregator dataAggregator) {
            super(looper);
            mDataAggregator = dataAggregator;
        }
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SINGLE_DATA:
                    mDataAggregator.onNewSingleData((ForensicEvent) msg.obj);
                    break;
                case MSG_BATCH_DATA:
                    mDataAggregator.onNewBatchData((List<ForensicEvent>) msg.obj);
                    break;
                case MSG_DISABLE:
                    mDataAggregator.onDisable();
                    break;
                default:
                    Slog.w(TAG, "Unknown message: " + msg.what);
            }
        }
    }
}
+29 −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.security.forensic;

public interface DataSource {
    /**
     * Enable the data collection.
     */
    void enable();

    /**
     * Disable the data collection.
     */
    void disable();
}
+31 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.security.forensic.ForensicEvent;
import android.security.forensic.IForensicService;
import android.security.forensic.IForensicServiceCommandCallback;
import android.security.forensic.IForensicServiceStateCallback;
@@ -32,6 +33,7 @@ import com.android.server.ServiceThread;
import com.android.server.SystemService;

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

/**
 * @hide
@@ -64,6 +66,7 @@ public class ForensicService extends SystemService {
    private final Context mContext;
    private final Handler mHandler;
    private final BackupTransportConnection mBackupTransportConnection;
    private final DataAggregator mDataAggregator;
    private final BinderService mBinderService;

    private final ArrayList<IForensicServiceStateCallback> mStateMonitors = new ArrayList<>();
@@ -79,6 +82,7 @@ public class ForensicService extends SystemService {
        mContext = injector.getContext();
        mHandler = new EventHandler(injector.getLooper(), this);
        mBackupTransportConnection = injector.getBackupTransportConnection();
        mDataAggregator = injector.getDataAggregator(this);
        mBinderService = new BinderService(this);
    }

@@ -167,6 +171,9 @@ public class ForensicService extends SystemService {
                        Slog.e(TAG, "RemoteException", e);
                    }
                    break;
                case MSG_BACKUP:
                    mService.backup((List<ForensicEvent>) msg.obj);
                    break;
                default:
                    Slog.w(TAG, "Unknown message: " + msg.what);
            }
@@ -192,6 +199,10 @@ public class ForensicService extends SystemService {
    private void makeVisible(IForensicServiceCommandCallback callback) throws RemoteException {
        switch (mState) {
            case STATE_INVISIBLE:
                if (!mDataAggregator.initialize()) {
                    callback.onFailure(ERROR_DATA_SOURCE_UNAVAILABLE);
                    break;
                }
                mState = STATE_VISIBLE;
                notifyStateMonitors();
                callback.onSuccess();
@@ -227,6 +238,7 @@ public class ForensicService extends SystemService {
                    callback.onFailure(ERROR_BACKUP_TRANSPORT_UNAVAILABLE);
                    break;
                }
                mDataAggregator.enable();
                mState = STATE_ENABLED;
                notifyStateMonitors();
                callback.onSuccess();
@@ -243,6 +255,7 @@ public class ForensicService extends SystemService {
        switch (mState) {
            case STATE_ENABLED:
                mBackupTransportConnection.release();
                mDataAggregator.disable();
                mState = STATE_VISIBLE;
                notifyStateMonitors();
                callback.onSuccess();
@@ -255,6 +268,17 @@ public class ForensicService extends SystemService {
        }
    }

    /**
     * Add a list of ForensicEvent.
     */
    public void addNewData(List<ForensicEvent> events) {
        mHandler.obtainMessage(MSG_BACKUP, events).sendToTarget();
    }

    private void backup(List<ForensicEvent> events) {
        mBackupTransportConnection.addData(events);
    }

    @Override
    public void onStart() {
        try {
@@ -275,6 +299,8 @@ public class ForensicService extends SystemService {
        Looper getLooper();

        BackupTransportConnection getBackupTransportConnection();

        DataAggregator getDataAggregator(ForensicService forensicService);
    }

    private static final class InjectorImpl implements Injector {
@@ -303,6 +329,11 @@ public class ForensicService extends SystemService {
        public BackupTransportConnection getBackupTransportConnection() {
            return new BackupTransportConnection(mContext);
        }

        @Override
        public DataAggregator getDataAggregator(ForensicService forensicService) {
            return new DataAggregator(forensicService);
        }
    }
}
+102 −1
Original line number Diff line number Diff line
@@ -19,23 +19,35 @@ package com.android.server.security.forensic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Looper;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.security.forensic.ForensicEvent;
import android.security.forensic.IForensicServiceCommandCallback;
import android.security.forensic.IForensicServiceStateCallback;
import android.util.ArrayMap;

import com.android.server.ServiceThread;

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

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

public class ForensicServiceTest {
    private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
    private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
@@ -55,10 +67,12 @@ public class ForensicServiceTest {
    @Mock
    private Context mContext;
    private BackupTransportConnection mBackupTransportConnection;

    private DataAggregator mDataAggregator;
    private ForensicService mForensicService;
    private TestLooper mTestLooper;
    private Looper mLooper;
    private TestLooper mTestLooperOfDataAggregator;
    private Looper mLooperOfDataAggregator;

    @SuppressLint("VisibleForTests")
    @Before
@@ -67,6 +81,8 @@ public class ForensicServiceTest {

        mTestLooper = new TestLooper();
        mLooper = mTestLooper.getLooper();
        mTestLooperOfDataAggregator = new TestLooper();
        mLooperOfDataAggregator = mTestLooperOfDataAggregator.getLooper();
        mForensicService = new ForensicService(new MockInjector(mContext));
        mForensicService.onStart();
    }
@@ -121,6 +137,8 @@ public class ForensicServiceTest {
        assertEquals(STATE_INVISIBLE, scb1.mState);
        assertEquals(STATE_INVISIBLE, scb2.mState);

        doReturn(true).when(mDataAggregator).initialize();

        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().makeVisible(ccb);
        mTestLooper.dispatchAll();
@@ -129,6 +147,29 @@ public class ForensicServiceTest {
        assertNull(ccb.mErrorCode);
    }

    @Test
    public void testMakeVisible_FromInvisible_TwoMonitors_DataSourceUnavailable()
            throws RemoteException {
        mForensicService.setState(STATE_INVISIBLE);
        StateCallback scb1 = new StateCallback();
        StateCallback scb2 = new StateCallback();
        mForensicService.getBinderService().monitorState(scb1);
        mForensicService.getBinderService().monitorState(scb2);
        mTestLooper.dispatchAll();
        assertEquals(STATE_INVISIBLE, scb1.mState);
        assertEquals(STATE_INVISIBLE, scb2.mState);

        doReturn(false).when(mDataAggregator).initialize();

        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().makeVisible(ccb);
        mTestLooper.dispatchAll();
        assertEquals(STATE_INVISIBLE, scb1.mState);
        assertEquals(STATE_INVISIBLE, scb2.mState);
        assertNotNull(ccb.mErrorCode);
        assertEquals(ERROR_DATA_SOURCE_UNAVAILABLE, ccb.mErrorCode.intValue());
    }

    @Test
    public void testMakeVisible_FromVisible_TwoMonitors() throws RemoteException {
        mForensicService.setState(STATE_VISIBLE);
@@ -262,6 +303,8 @@ public class ForensicServiceTest {
        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().enable(ccb);
        mTestLooper.dispatchAll();

        verify(mDataAggregator, times(1)).enable();
        assertEquals(STATE_ENABLED, scb1.mState);
        assertEquals(STATE_ENABLED, scb2.mState);
        assertNull(ccb.mErrorCode);
@@ -361,14 +404,67 @@ public class ForensicServiceTest {

        doNothing().when(mBackupTransportConnection).release();

        ServiceThread mockThread = spy(ServiceThread.class);
        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);

        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().disable(ccb);
        mTestLooper.dispatchAll();
        mTestLooperOfDataAggregator.dispatchAll();
        // TODO: We can verify the data sources once we implement them.
        verify(mockThread, times(1)).quitSafely();
        assertEquals(STATE_VISIBLE, scb1.mState);
        assertEquals(STATE_VISIBLE, scb2.mState);
        assertNull(ccb.mErrorCode);
    }

    @Test
    public void testDataAggregator_AddBatchData() {
        mForensicService.setState(STATE_ENABLED);
        ServiceThread mockThread = spy(ServiceThread.class);
        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);

        String eventOneType = "event_one_type";
        String eventOneMapKey = "event_one_map_key";
        String eventOneMapVal = "event_one_map_val";
        Map<String, String> eventOneMap = new ArrayMap<String, String>();
        eventOneMap.put(eventOneMapKey, eventOneMapVal);
        ForensicEvent eventOne = new ForensicEvent(eventOneType, eventOneMap);

        String eventTwoType = "event_two_type";
        String eventTwoMapKey = "event_two_map_key";
        String eventTwoMapVal = "event_two_map_val";
        Map<String, String> eventTwoMap = new ArrayMap<String, String>();
        eventTwoMap.put(eventTwoMapKey, eventTwoMapVal);
        ForensicEvent eventTwo = new ForensicEvent(eventTwoType, eventTwoMap);

        List<ForensicEvent> events = new ArrayList<>();
        events.add(eventOne);
        events.add(eventTwo);

        doReturn(true).when(mBackupTransportConnection).addData(any());

        mDataAggregator.addBatchData(events);
        mTestLooperOfDataAggregator.dispatchAll();
        mTestLooper.dispatchAll();

        ArgumentCaptor<List<ForensicEvent>> captor = ArgumentCaptor.forClass(List.class);
        verify(mBackupTransportConnection).addData(captor.capture());
        List<ForensicEvent> receivedEvents = captor.getValue();
        assertEquals(receivedEvents.size(), 2);

        assertEquals(receivedEvents.getFirst().getType(), eventOneType);
        assertEquals(receivedEvents.getFirst().getKeyValuePairs().size(), 1);
        assertEquals(receivedEvents.getFirst().getKeyValuePairs().get(eventOneMapKey),
                eventOneMapVal);

        assertEquals(receivedEvents.getLast().getType(), eventTwoType);
        assertEquals(receivedEvents.getLast().getKeyValuePairs().size(), 1);
        assertEquals(receivedEvents.getLast().getKeyValuePairs().get(eventTwoMapKey),
                eventTwoMapVal);

    }

    private class MockInjector implements ForensicService.Injector {
        private final Context mContext;

@@ -393,6 +489,11 @@ public class ForensicServiceTest {
            return mBackupTransportConnection;
        }

        @Override
        public DataAggregator getDataAggregator(ForensicService forensicService) {
            mDataAggregator = spy(new DataAggregator(forensicService));
            return mDataAggregator;
        }
    }

    private static class StateCallback extends IForensicServiceStateCallback.Stub {