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

Commit edc79612 authored by Tianjie Xu's avatar Tianjie Xu Committed by Gerrit Code Review
Browse files

Merge "Create the ResumeOnRebootService."

parents 923d7d28 91d8ff41
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -44,6 +44,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";
@@ -9063,6 +9064,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.settings.suggestions {
  public final class Suggestion implements android.os.Parcelable {
+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
@@ -2984,6 +2984,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