diff options
| -rw-r--r-- | core/java/android/service/attestation/ImpressionAttestationService.java | 8 | ||||
| -rw-r--r-- | services/core/java/com/android/server/wm/ImpressionAttestationController.java | 356 |
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); + } + } + +} |