diff options
5 files changed, 312 insertions, 1 deletions
diff --git a/core/java/android/security/forensic/ForensicEvent.java b/core/java/android/security/forensic/ForensicEvent.java index 9cbc5ecea962..90906edcc636 100644 --- a/core/java/android/security/forensic/ForensicEvent.java +++ b/core/java/android/security/forensic/ForensicEvent.java @@ -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); diff --git a/services/core/java/com/android/server/security/forensic/DataAggregator.java b/services/core/java/com/android/server/security/forensic/DataAggregator.java new file mode 100644 index 000000000000..0079818e733b --- /dev/null +++ b/services/core/java/com/android/server/security/forensic/DataAggregator.java @@ -0,0 +1,142 @@ +/* + * 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); + } + } + } +} diff --git a/services/core/java/com/android/server/security/forensic/DataSource.java b/services/core/java/com/android/server/security/forensic/DataSource.java new file mode 100644 index 000000000000..da7ee210281b --- /dev/null +++ b/services/core/java/com/android/server/security/forensic/DataSource.java @@ -0,0 +1,29 @@ +/* + * 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(); +} diff --git a/services/core/java/com/android/server/security/forensic/ForensicService.java b/services/core/java/com/android/server/security/forensic/ForensicService.java index 20c648eb61c2..53b07c04139c 100644 --- a/services/core/java/com/android/server/security/forensic/ForensicService.java +++ b/services/core/java/com/android/server/security/forensic/ForensicService.java @@ -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); + } } } diff --git a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java index 2b55303bd89f..feb00e760ce6 100644 --- a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java +++ b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java @@ -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(); @@ -130,6 +148,29 @@ public class ForensicServiceTest { } @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); StateCallback scb1 = new StateCallback(); @@ -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 { |