summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/service/attestation/ImpressionAttestationService.java8
-rw-r--r--services/core/java/com/android/server/wm/ImpressionAttestationController.java356
2 files changed, 364 insertions, 0 deletions
diff --git a/core/java/android/service/attestation/ImpressionAttestationService.java b/core/java/android/service/attestation/ImpressionAttestationService.java
index 4919f5d8856f..923ab7a65d1b 100644
--- a/core/java/android/service/attestation/ImpressionAttestationService.java
+++ b/core/java/android/service/attestation/ImpressionAttestationService.java
@@ -71,6 +71,14 @@ public abstract class ImpressionAttestationService extends Service {
public static final String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS =
"android.attestation.available_algorithms";
+ /**
+ * The {@link Intent} action that must be declared as handled by a service in its manifest
+ * for the system to recognize it as an impression attestation providing service.
+ * @hide
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.attestation.ImpressionAttestationService";
+
private ImpressionAttestationServiceWrapper mWrapper;
private Handler mHandler;
diff --git a/services/core/java/com/android/server/wm/ImpressionAttestationController.java b/services/core/java/com/android/server/wm/ImpressionAttestationController.java
new file mode 100644
index 000000000000..d00faef1c147
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ImpressionAttestationController.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2020 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.wm;
+
+import static android.service.attestation.ImpressionAttestationService.SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.attestation.IImpressionAttestationService;
+import android.service.attestation.ImpressionAttestationService;
+import android.service.attestation.ImpressionToken;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+/**
+ * Handles requests into {@link ImpressionAttestationService}
+ *
+ * Do not hold the {@link WindowManagerService#mGlobalLock} when calling methods since they are
+ * blocking calls into another service.
+ */
+public class ImpressionAttestationController {
+ private static final String TAG = "ImpressionAttestationController";
+ private static final boolean DEBUG = false;
+
+ private final Object mServiceConnectionLock = new Object();
+
+ @GuardedBy("mServiceConnectionLock")
+ private ImpressionAttestationServiceConnection mServiceConnection;
+
+ private final Context mContext;
+
+ /**
+ * Lock used for the cached {@link #mImpressionAlgorithms} array
+ */
+ private final Object mImpressionAlgorithmsLock = new Object();
+
+ @GuardedBy("mImpressionAlgorithmsLock")
+ private String[] mImpressionAlgorithms;
+
+ private final Handler mHandler;
+
+ private interface Command {
+ void run(IImpressionAttestationService service) throws RemoteException;
+ }
+
+ ImpressionAttestationController(Context context) {
+ mContext = context;
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ String[] getSupportedImpressionAlgorithms() {
+ // We have a separate lock for the impression algorithm array since it doesn't need to make
+ // the request through the service connection. Instead, we have a lock to ensure we can
+ // properly cache the impression algorithms array so we don't need to call into the
+ // ExtServices process for each request.
+ synchronized (mImpressionAlgorithmsLock) {
+ // Already have cached values
+ if (mImpressionAlgorithms != null) {
+ return mImpressionAlgorithms;
+ }
+
+ final ServiceInfo serviceInfo = getServiceInfo();
+ if (serviceInfo == null) return null;
+
+ final PackageManager pm = mContext.getPackageManager();
+ final Resources res;
+ try {
+ res = pm.getResourcesForApplication(serviceInfo.applicationInfo);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Error getting application resources for " + serviceInfo, e);
+ return null;
+ }
+
+ final int resourceId = serviceInfo.metaData.getInt(
+ SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS);
+ mImpressionAlgorithms = res.getStringArray(resourceId);
+
+ return mImpressionAlgorithms;
+ }
+ }
+
+ int verifyImpressionToken(ImpressionToken impressionToken) {
+ final SyncCommand syncCommand = new SyncCommand();
+ Bundle results = syncCommand.run((service, remoteCallback) -> {
+ try {
+ service.verifyImpressionToken(impressionToken, remoteCallback);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to invoke verifyImpressionToken command");
+ }
+ });
+
+ return results.getInt(ImpressionAttestationService.EXTRA_VERIFICATION_STATUS);
+ }
+
+ ImpressionToken generateImpressionToken(HardwareBuffer screenshot, Rect bounds,
+ String hashAlgorithm) {
+ final SyncCommand syncCommand = new SyncCommand();
+ Bundle results = syncCommand.run((service, remoteCallback) -> {
+ try {
+ service.generateImpressionToken(screenshot, bounds, hashAlgorithm, remoteCallback);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to invoke generateImpressionToken command", e);
+ }
+ });
+
+ return results.getParcelable(ImpressionAttestationService.EXTRA_IMPRESSION_TOKEN);
+ }
+
+ /**
+ * Run a command, starting the service connection if necessary.
+ */
+ private void connectAndRun(@NonNull Command command) {
+ synchronized (mServiceConnectionLock) {
+ mHandler.resetTimeoutMessage();
+ if (mServiceConnection == null) {
+ if (DEBUG) Slog.v(TAG, "creating connection");
+
+ // Create the connection
+ mServiceConnection = new ImpressionAttestationServiceConnection();
+
+ final ComponentName component = getServiceComponentName();
+ if (DEBUG) Slog.v(TAG, "binding to: " + component);
+ if (component != null) {
+ final Intent intent = new Intent();
+ intent.setComponent(component);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mContext.bindServiceAsUser(intent, mServiceConnection,
+ Context.BIND_AUTO_CREATE, UserHandle.CURRENT);
+ if (DEBUG) Slog.v(TAG, "bound");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ mServiceConnection.runCommandLocked(command);
+ }
+ }
+
+ @Nullable
+ private ServiceInfo getServiceInfo() {
+ final String packageName =
+ mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
+ if (packageName == null) {
+ Slog.w(TAG, "no external services package!");
+ return null;
+ }
+
+ final Intent intent = new Intent(ImpressionAttestationService.SERVICE_INTERFACE);
+ intent.setPackage(packageName);
+ final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ Slog.w(TAG, "No valid components found.");
+ return null;
+ }
+ return resolveInfo.serviceInfo;
+ }
+
+ @Nullable
+ private ComponentName getServiceComponentName() {
+ final ServiceInfo serviceInfo = getServiceInfo();
+ if (serviceInfo == null) return null;
+
+ final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ if (!Manifest.permission.BIND_IMPRESSION_ATTESTATION_SERVICE
+ .equals(serviceInfo.permission)) {
+ Slog.w(TAG, name.flattenToShortString() + " requires permission "
+ + Manifest.permission.BIND_IMPRESSION_ATTESTATION_SERVICE);
+ return null;
+ }
+
+ if (DEBUG) Slog.v(TAG, "getServiceComponentName(): " + name);
+ return name;
+ }
+
+ private class SyncCommand {
+ private static final int WAIT_TIME_S = 5;
+ private Bundle mResult;
+ private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ public Bundle run(BiConsumer<IImpressionAttestationService, RemoteCallback> func) {
+ connectAndRun(service -> {
+ RemoteCallback callback = new RemoteCallback(result -> {
+ mResult = result;
+ mCountDownLatch.countDown();
+ });
+ func.accept(service, callback);
+ });
+
+ try {
+ mCountDownLatch.await(WAIT_TIME_S, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to wait for command", e);
+ }
+
+ return mResult;
+ }
+ }
+
+ private class ImpressionAttestationServiceConnection implements ServiceConnection {
+ @GuardedBy("mServiceConnectionLock")
+ private IImpressionAttestationService mRemoteService;
+
+ @GuardedBy("mServiceConnectionLock")
+ private ArrayList<Command> mQueuedCommands;
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) Slog.v(TAG, "onServiceConnected(): " + name);
+ synchronized (mServiceConnectionLock) {
+ mRemoteService = IImpressionAttestationService.Stub.asInterface(service);
+ if (mQueuedCommands != null) {
+ final int size = mQueuedCommands.size();
+ if (DEBUG) Slog.d(TAG, "running " + size + " queued commands");
+ for (int i = 0; i < size; i++) {
+ final Command queuedCommand = mQueuedCommands.get(i);
+ try {
+ if (DEBUG) Slog.v(TAG, "running queued command #" + i);
+ queuedCommand.run(mRemoteService);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "exception calling " + name + ": " + e);
+ }
+ }
+ mQueuedCommands = null;
+ } else if (DEBUG) {
+ Slog.d(TAG, "no queued commands");
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Slog.v(TAG, "onServiceDisconnected(): " + name);
+ synchronized (mServiceConnectionLock) {
+ mRemoteService = null;
+ }
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ if (DEBUG) Slog.v(TAG, "onBindingDied(): " + name);
+ synchronized (mServiceConnectionLock) {
+ mRemoteService = null;
+ }
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ if (DEBUG) Slog.v(TAG, "onNullBinding(): " + name);
+ synchronized (mServiceConnectionLock) {
+ mRemoteService = null;
+ }
+ }
+
+ /**
+ * Only call while holding {@link #mServiceConnectionLock}
+ */
+ private void runCommandLocked(Command command) {
+ if (mRemoteService == null) {
+ if (DEBUG) Slog.d(TAG, "service is null; queuing command");
+ if (mQueuedCommands == null) {
+ mQueuedCommands = new ArrayList<>(1);
+ }
+ mQueuedCommands.add(command);
+ } else {
+ try {
+ if (DEBUG) Slog.v(TAG, "running command right away");
+ command.run(mRemoteService);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "exception calling service: " + e);
+ }
+ }
+ }
+ }
+
+ private class Handler extends android.os.Handler {
+ static final long SERVICE_SHUTDOWN_TIMEOUT_MILLIS = 10000; // 10s
+ static final int MSG_SERVICE_SHUTDOWN_TIMEOUT = 1;
+
+ Handler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_SERVICE_SHUTDOWN_TIMEOUT) {
+ if (DEBUG) {
+ Slog.v(TAG, "Shutting down service");
+ }
+ synchronized (mServiceConnectionLock) {
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ mServiceConnection = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Set a timer for {@link #SERVICE_SHUTDOWN_TIMEOUT_MILLIS} so we can tear down the service
+ * if it's inactive. The requests will be coming from apps so it's hard to tell how often
+ * the requests can come in. Therefore, we leave the service running if requests continue
+ * to come in. Once there's been no activity for 10s, we can shut down the service and
+ * restart when we get a new request.
+ */
+ void resetTimeoutMessage() {
+ if (DEBUG) {
+ Slog.v(TAG, "Reset shutdown message");
+ }
+ removeMessages(MSG_SERVICE_SHUTDOWN_TIMEOUT);
+ sendEmptyMessageDelayed(MSG_SERVICE_SHUTDOWN_TIMEOUT, SERVICE_SHUTDOWN_TIMEOUT_MILLIS);
+ }
+ }
+
+}