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

Commit ade3051c authored by chaviw's avatar chaviw
Browse files

Bind ImpressionAttestationService to system

Added ImpressionAttestationController in WindowManager to allow the
ImpressionAttestationService to bind to system.

Added helper methods in ImpressionAttestationController:
1. Make requests blocking since they will come from a binder request and
can block without causing issues in WindowManager

2. Added timeout so the service will get torn down after 10s of
inactivity

3. Added cache of hashing algorithms list so they can be retrieved
without making another IPC call.

Test: Builds
Bug: 155825630
Change-Id: Ic80206fb84a8f020f45d5aa053ee86a005da4591
parent cb31f42b
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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;

+356 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }

}