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

Commit 4ace4790 authored by Jiachen Zhao's avatar Jiachen Zhao
Browse files

Create the ResumeOnRebootService.

This is the base class for service that provides wrapping/unwrapping of the opaque blob needed for
ResumeOnReboot operation. The package needs to provide a wrap and unwrap
implementation for handling the opaque blob, that's secure even when on
device keystore and clock is compromised. This can be achieved by using
tamper-resistant hardware such as a secure element with a secure clock,
or using a remote server to store and retrieve data and manage timing.

Bug: 172780686
Test: atest FrameworksServicesTests:ResumeOnRebootServiceProviderTests
Change-Id: I98378be6963194c2e6faef8ebc441066b75a0bbf
parent 0b28f5f3
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ package android {
    field public static final String BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE = "android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE";
    field public static final String BIND_PRINT_RECOMMENDATION_SERVICE = "android.permission.BIND_PRINT_RECOMMENDATION_SERVICE";
    field public static final String BIND_RESOLVER_RANKER_SERVICE = "android.permission.BIND_RESOLVER_RANKER_SERVICE";
    field public static final String BIND_RESUME_ON_REBOOT_SERVICE = "android.permission.BIND_RESUME_ON_REBOOT_SERVICE";
    field public static final String BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE = "android.permission.BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE";
    field public static final String BIND_SETTINGS_SUGGESTIONS_SERVICE = "android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE";
    field public static final String BIND_SOUND_TRIGGER_DETECTION_SERVICE = "android.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE";
@@ -9886,6 +9887,18 @@ package android.service.resolver {
}
package android.service.resumeonreboot {
  public abstract class ResumeOnRebootService extends android.app.Service {
    ctor public ResumeOnRebootService();
    method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent);
    method @NonNull public abstract byte[] onUnwrap(@NonNull byte[]) throws java.io.IOException;
    method @NonNull public abstract byte[] onWrap(@NonNull byte[], long) throws java.io.IOException;
    field public static final String SERVICE_INTERFACE = "android.service.resumeonreboot.ResumeOnRebootService";
  }
}
package android.service.search {
  public abstract class SearchUiService extends android.app.Service {
+25 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.service.resumeonreboot;

import android.os.RemoteCallback;

/** @hide */
interface IResumeOnRebootService {
    oneway void wrapSecret(in byte[] unwrappedBlob, in long lifeTimeInMillis, in RemoteCallback resultCallback);
    oneway void unwrap(in byte[] wrappedBlob, in RemoteCallback resultCallback);
}
 No newline at end of file
+164 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.service.resumeonreboot;

import android.annotation.DurationMillisLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelableException;
import android.os.RemoteCallback;
import android.os.RemoteException;

import com.android.internal.os.BackgroundThread;

import java.io.IOException;

/**
 * Base class for service that provides wrapping/unwrapping of the opaque blob needed for
 * ResumeOnReboot operation. The package needs to provide a wrap/unwrap implementation for handling
 * the opaque blob, that's secure even when on device keystore and clock is compromised. This can
 * be achieved by using tamper-resistant hardware such as a secure element with a secure clock, or
 * using a remote server to store and retrieve data and manage timing.
 *
 * <p>To extend this class, you must declare the service in your manifest file with the
 * {@link android.Manifest.permission#BIND_RESUME_ON_REBOOT_SERVICE} permission,
 * include an intent filter with the {@link #SERVICE_INTERFACE} action and mark the service as
 * direct-boot aware. In addition, the package that contains the service must be granted
 * {@link android.Manifest.permission#BIND_RESUME_ON_REBOOT_SERVICE}.
 * For example:</p>
 * <pre>
 *     &lt;service android:name=".FooResumeOnRebootService"
 *             android:exported="true"
 *             android:priority="100"
 *             android:directBootAware="true"
 *             android:permission="android.permission.BIND_RESUME_ON_REBOOT_SERVICE"&gt;
 *         &lt;intent-filter&gt;
 *             &lt;action android:name="android.service.resumeonreboot.ResumeOnRebootService" /&gt;
 *         &lt;/intent-filter&gt;
 *     &lt;/service&gt;
 * </pre>
 *
 * //TODO: Replace this with public link when available.
 *
 * @hide
 * @see
 * <a href="https://goto.google.com/server-based-ror">https://goto.google.com/server-based-ror</a>
 */
@SystemApi
public abstract class ResumeOnRebootService extends Service {

    /**
     * The intent that the service must respond to. Add it to the intent filter of the service.
     */
    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
    public static final String SERVICE_INTERFACE =
            "android.service.resumeonreboot.ResumeOnRebootService";
    /** @hide */
    public static final String UNWRAPPED_BLOB_KEY = "unrwapped_blob_key";
    /** @hide */
    public static final String WRAPPED_BLOB_KEY = "wrapped_blob_key";
    /** @hide */
    public static final String EXCEPTION_KEY = "exception_key";

    private final Handler mHandler = BackgroundThread.getHandler();

    /**
     * Implementation for wrapping the opaque blob used for resume-on-reboot prior to
     * reboot. The service should not assume any structure of the blob to be wrapped. The
     * implementation should wrap the opaque blob in a reasonable time or throw {@link IOException}
     * if it's unable to complete the action.
     *
     * @param blob             The opaque blob with size on the order of 100 bytes.
     * @param lifeTimeInMillis The life time of the blob. This must be strictly enforced by the
     *                         implementation and any attempt to unWrap the wrapped blob returned by
     *                         this function after expiration should
     *                         fail.
     * @return Wrapped blob to be persisted across reboot with size on the order of 100 bytes.
     * @throws IOException if the implementation is unable to wrap the blob successfully.
     */
    @NonNull
    public abstract byte[] onWrap(@NonNull byte[] blob, @DurationMillisLong long lifeTimeInMillis)
            throws IOException;

    /**
     * Implementation for unwrapping the wrapped blob used for resume-on-reboot after reboot. This
     * operation would happen after reboot during direct boot mode (i.e before device is unlocked
     * for the first time). The implementation should unwrap the wrapped blob in a reasonable time
     * and returns the result or throw {@link IOException} if it's unable to complete the action
     * and {@link IllegalArgumentException} if {@code unwrapBlob} fails because the wrappedBlob is
     * stale.
     *
     * @param wrappedBlob The wrapped blob with size on the order of 100 bytes.
     * @return Unwrapped blob used for resume-on-reboot with the size on the order of 100 bytes.
     * @throws IOException if the implementation is unable to unwrap the wrapped blob successfully.
     */
    @NonNull
    public abstract byte[] onUnwrap(@NonNull byte[] wrappedBlob) throws IOException;

    private final android.service.resumeonreboot.IResumeOnRebootService mInterface =
            new android.service.resumeonreboot.IResumeOnRebootService.Stub() {

                @Override
                public void wrapSecret(byte[] unwrappedBlob,
                        @DurationMillisLong long lifeTimeInMillis,
                        RemoteCallback resultCallback) throws RemoteException {
                    mHandler.post(() -> {
                        try {
                            byte[] wrappedBlob = onWrap(unwrappedBlob,
                                    lifeTimeInMillis);
                            Bundle bundle = new Bundle();
                            bundle.putByteArray(WRAPPED_BLOB_KEY, wrappedBlob);
                            resultCallback.sendResult(bundle);
                        } catch (Throwable e) {
                            Bundle bundle = new Bundle();
                            bundle.putParcelable(EXCEPTION_KEY, new ParcelableException(e));
                            resultCallback.sendResult(bundle);
                        }
                    });
                }

                @Override
                public void unwrap(byte[] wrappedBlob, RemoteCallback resultCallback)
                        throws RemoteException {
                    mHandler.post(() -> {
                        try {
                            byte[] unwrappedBlob = onUnwrap(wrappedBlob);
                            Bundle bundle = new Bundle();
                            bundle.putByteArray(UNWRAPPED_BLOB_KEY, unwrappedBlob);
                            resultCallback.sendResult(bundle);
                        } catch (Throwable e) {
                            Bundle bundle = new Bundle();
                            bundle.putParcelable(EXCEPTION_KEY, new ParcelableException(e));
                            resultCallback.sendResult(bundle);
                        }
                    });
                }
            };

    @Nullable
    @Override
    public IBinder onBind(@Nullable Intent intent) {
        return mInterface.asBinder();
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -3109,6 +3109,12 @@
    <permission android:name="android.permission.RECOVERY"
        android:protectionLevel="signature|privileged" />

    <!-- @SystemApi Allows an application to do certain operations needed for
         resume on reboot feature.
         @hide -->
    <permission android:name="android.permission.BIND_RESUME_ON_REBOOT_SERVICE"
        android:protectionLevel="signature" />

    <!-- @SystemApi Allows an application to read system update info.
         @hide -->
    <permission android:name="android.permission.READ_SYSTEM_UPDATE_INFO"
+249 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.locksettings;

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.os.Bundle;
import android.os.IBinder;
import android.os.ParcelableException;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.service.resumeonreboot.IResumeOnRebootService;
import android.service.resumeonreboot.ResumeOnRebootService;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/** @hide */
public class ResumeOnRebootServiceProvider {

    private static final String PROVIDER_PACKAGE = DeviceConfig.getString(
            DeviceConfig.NAMESPACE_OTA, "resume_on_reboot_service_package", "");
    private static final String PROVIDER_REQUIRED_PERMISSION =
            Manifest.permission.BIND_RESUME_ON_REBOOT_SERVICE;
    private static final String TAG = "ResumeOnRebootServiceProvider";

    private final Context mContext;
    private final PackageManager mPackageManager;

    public ResumeOnRebootServiceProvider(Context context) {
        this(context, context.getPackageManager());
    }

    @VisibleForTesting
    public ResumeOnRebootServiceProvider(Context context, PackageManager packageManager) {
        this.mContext = context;
        this.mPackageManager = packageManager;
    }

    @Nullable
    private ServiceInfo resolveService() {
        Intent intent = new Intent();
        intent.setAction(ResumeOnRebootService.SERVICE_INTERFACE);
        if (PROVIDER_PACKAGE != null && !PROVIDER_PACKAGE.equals("")) {
            intent.setPackage(PROVIDER_PACKAGE);
        }

        List<ResolveInfo> resolvedIntents =
                mPackageManager.queryIntentServices(intent, PackageManager.MATCH_SYSTEM_ONLY);
        for (ResolveInfo resolvedInfo : resolvedIntents) {
            if (resolvedInfo.serviceInfo != null
                    && PROVIDER_REQUIRED_PERMISSION.equals(resolvedInfo.serviceInfo.permission)) {
                return resolvedInfo.serviceInfo;
            }
        }
        return null;
    }

    /** Creates a new {@link ResumeOnRebootServiceConnection} */
    @Nullable
    public ResumeOnRebootServiceConnection getServiceConnection() {
        ServiceInfo serviceInfo = resolveService();
        if (serviceInfo == null) {
            return null;
        }
        return new ResumeOnRebootServiceConnection(mContext, serviceInfo.getComponentName());
    }

    /**
     * Connection class used for contacting the registered {@link IResumeOnRebootService}
     */
    public static class ResumeOnRebootServiceConnection {

        private static final String TAG = "ResumeOnRebootServiceConnection";
        private final Context mContext;
        private final ComponentName mComponentName;
        private IResumeOnRebootService mBinder;

        private ResumeOnRebootServiceConnection(Context context,
                @NonNull ComponentName componentName) {
            mContext = context;
            mComponentName = componentName;
        }

        /** Unbind from the service */
        public void unbindService() {
            mContext.unbindService(new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {
                    mBinder = null;

                }
            });
        }

        /** Bind to the service */
        public void bindToService(long timeOut) throws TimeoutException {
            if (mBinder == null || !mBinder.asBinder().isBinderAlive()) {
                CountDownLatch connectionLatch = new CountDownLatch(1);
                Intent intent = new Intent();
                intent.setComponent(mComponentName);
                final boolean success = mContext.bindServiceAsUser(intent, new ServiceConnection() {
                            @Override
                            public void onServiceConnected(ComponentName name, IBinder service) {
                                mBinder = IResumeOnRebootService.Stub.asInterface(service);
                                connectionLatch.countDown();
                            }

                            @Override
                            public void onServiceDisconnected(ComponentName name) {
                            }
                        },
                        Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
                        BackgroundThread.getHandler(), UserHandle.SYSTEM);

                if (!success) {
                    Slog.e(TAG, "Binding: " + mComponentName + " u" + UserHandle.SYSTEM
                            + " failed.");
                    return;
                }
                waitForLatch(connectionLatch, "serviceConnection", timeOut);
            }
        }

        /** Wrap opaque blob */
        public byte[] wrapBlob(byte[] unwrappedBlob, long lifeTimeInMillis,
                long timeOutInMillis)
                throws RemoteException, TimeoutException, IOException {
            if (mBinder == null || !mBinder.asBinder().isBinderAlive()) {
                throw new RemoteException("Service not bound");
            }
            CountDownLatch binderLatch = new CountDownLatch(1);
            ResumeOnRebootServiceCallback
                    resultCallback =
                    new ResumeOnRebootServiceCallback(
                            binderLatch);
            mBinder.wrapSecret(unwrappedBlob, lifeTimeInMillis, new RemoteCallback(resultCallback));
            waitForLatch(binderLatch, "wrapSecret", timeOutInMillis);
            if (resultCallback.getResult().containsKey(ResumeOnRebootService.EXCEPTION_KEY)) {
                throwTypedException(resultCallback.getResult().getParcelable(
                        ResumeOnRebootService.EXCEPTION_KEY));
            }
            return resultCallback.mResult.getByteArray(ResumeOnRebootService.WRAPPED_BLOB_KEY);
        }

        /** Unwrap wrapped blob */
        public byte[] unwrap(byte[] wrappedBlob, long timeOut)
                throws RemoteException, TimeoutException, IOException {
            if (mBinder == null || !mBinder.asBinder().isBinderAlive()) {
                throw new RemoteException("Service not bound");
            }
            CountDownLatch binderLatch = new CountDownLatch(1);
            ResumeOnRebootServiceCallback
                    resultCallback =
                    new ResumeOnRebootServiceCallback(
                            binderLatch);
            mBinder.unwrap(wrappedBlob, new RemoteCallback(resultCallback));
            waitForLatch(binderLatch, "unWrapSecret", timeOut);
            if (resultCallback.getResult().containsKey(ResumeOnRebootService.EXCEPTION_KEY)) {
                throwTypedException(resultCallback.getResult().getParcelable(
                        ResumeOnRebootService.EXCEPTION_KEY));
            }
            return resultCallback.getResult().getByteArray(
                    ResumeOnRebootService.UNWRAPPED_BLOB_KEY);
        }

        private void throwTypedException(
                ParcelableException exception)
                throws IOException {
            if (exception.getCause() instanceof IOException) {
                exception.maybeRethrow(IOException.class);
            } else if (exception.getCause() instanceof IllegalStateException) {
                exception.maybeRethrow(IllegalStateException.class);
            } else {
                // This should not happen. Wrap the cause in IllegalStateException so that it
                // doesn't disrupt the exception handling
                throw new IllegalStateException(exception.getCause());
            }
        }

        private void waitForLatch(CountDownLatch latch, String reason, long timeOut)
                throws TimeoutException {
            try {
                if (!latch.await(timeOut, TimeUnit.SECONDS)) {
                    throw new TimeoutException("Latch wait for " + reason + " elapsed");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("Latch wait for " + reason + " interrupted");
            }
        }
    }

    private static class ResumeOnRebootServiceCallback implements
            RemoteCallback.OnResultListener {

        private final CountDownLatch mResultLatch;
        private Bundle mResult;

        private ResumeOnRebootServiceCallback(CountDownLatch resultLatch) {
            this.mResultLatch = resultLatch;
        }

        @Override
        public void onResult(@Nullable Bundle result) {
            this.mResult = result;
            mResultLatch.countDown();
        }

        private Bundle getResult() {
            return mResult;
        }
    }
}
Loading