blob: e0701e867cad70e9bf6ffb1cbd16e50ad6ff2aeb [file] [log] [blame]
/*
* Copyright (C) 2016 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.recoverysystem;
import android.content.Context;
import android.content.IntentSender;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Binder;
import android.os.IRecoverySystem;
import android.os.IRecoverySystemProgressListener;
import android.os.PowerManager;
import android.os.Process;
import android.os.RecoverySystem;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemProperties;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockSettingsInternal;
import com.android.internal.widget.RebootEscrowListener;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import libcore.io.IoUtils;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileDescriptor;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* The recovery system service is responsible for coordinating recovery related
* functions on the device. It sets up (or clears) the bootloader control block
* (BCB), which will be read by the bootloader and the recovery image. It also
* triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
* /data partition so that it can be accessed under the recovery image.
*/
public class RecoverySystemService extends IRecoverySystem.Stub implements RebootEscrowListener {
private static final String TAG = "RecoverySystemService";
private static final boolean DEBUG = false;
// The socket at /dev/socket/uncrypt to communicate with uncrypt.
private static final String UNCRYPT_SOCKET = "uncrypt";
// The init services that communicate with /system/bin/uncrypt.
@VisibleForTesting
static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
@VisibleForTesting
static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
@VisibleForTesting
static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
private static final Object sRequestLock = new Object();
private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
private final Injector mInjector;
private final Context mContext;
private boolean mPreparedForReboot;
private String mUnattendedRebootToken;
private IntentSender mPreparedForRebootIntentSender;
static class Injector {
protected final Context mContext;
Injector(Context context) {
mContext = context;
}
public Context getContext() {
return mContext;
}
public LockSettingsInternal getLockSettingsService() {
return LocalServices.getService(LockSettingsInternal.class);
}
public PowerManager getPowerManager() {
return (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
}
public String systemPropertiesGet(String key) {
return SystemProperties.get(key);
}
public void systemPropertiesSet(String key, String value) {
SystemProperties.set(key, value);
}
public boolean uncryptPackageFileDelete() {
return RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
}
public String getUncryptPackageFileName() {
return RecoverySystem.UNCRYPT_PACKAGE_FILE.getName();
}
public FileWriter getUncryptPackageFileWriter() throws IOException {
return new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE);
}
public UncryptSocket connectService() {
UncryptSocket socket = new UncryptSocket();
if (!socket.connectService()) {
socket.close();
return null;
}
return socket;
}
public void threadSleep(long millis) throws InterruptedException {
Thread.sleep(millis);
}
}
/**
* Handles the lifecycle events for the RecoverySystemService.
*/
public static final class Lifecycle extends SystemService {
private RecoverySystemService mRecoverySystemService;
public Lifecycle(Context context) {
super(context);
}
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
mRecoverySystemService.onSystemServicesReady();
}
}
@Override
public void onStart() {
mRecoverySystemService = new RecoverySystemService(getContext());
publishBinderService(Context.RECOVERY_SERVICE, mRecoverySystemService);
}
}
private RecoverySystemService(Context context) {
this(new Injector(context));
}
@VisibleForTesting
RecoverySystemService(Injector injector) {
mInjector = injector;
mContext = injector.getContext();
}
@VisibleForTesting
void onSystemServicesReady() {
mInjector.getLockSettingsService().setRebootEscrowListener(this);
}
@Override // Binder call
public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
synchronized (sRequestLock) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
if (!checkAndWaitForUncryptService()) {
Slog.e(TAG, "uncrypt service is unavailable.");
return false;
}
// Write the filename into uncrypt package file to be read by
// uncrypt.
mInjector.uncryptPackageFileDelete();
try (FileWriter uncryptFile = mInjector.getUncryptPackageFileWriter()) {
uncryptFile.write(filename + "\n");
} catch (IOException e) {
Slog.e(TAG, "IOException when writing \""
+ mInjector.getUncryptPackageFileName() + "\":", e);
return false;
}
// Trigger uncrypt via init.
mInjector.systemPropertiesSet("ctl.start", "uncrypt");
// Connect to the uncrypt service socket.
UncryptSocket socket = mInjector.connectService();
if (socket == null) {
Slog.e(TAG, "Failed to connect to uncrypt socket");
return false;
}
// Read the status from the socket.
try {
int lastStatus = Integer.MIN_VALUE;
while (true) {
int status = socket.getPercentageUncrypted();
// Avoid flooding the log with the same message.
if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
continue;
}
lastStatus = status;
if (status >= 0 && status <= 100) {
// Update status
Slog.i(TAG, "uncrypt read status: " + status);
if (listener != null) {
try {
listener.onProgress(status);
} catch (RemoteException ignored) {
Slog.w(TAG, "RemoteException when posting progress");
}
}
if (status == 100) {
Slog.i(TAG, "uncrypt successfully finished.");
// Ack receipt of the final status code. uncrypt
// waits for the ack so the socket won't be
// destroyed before we receive the code.
socket.sendAck();
break;
}
} else {
// Error in /system/bin/uncrypt.
Slog.e(TAG, "uncrypt failed with status: " + status);
// Ack receipt of the final status code. uncrypt waits
// for the ack so the socket won't be destroyed before
// we receive the code.
socket.sendAck();
return false;
}
}
} catch (IOException e) {
Slog.e(TAG, "IOException when reading status: ", e);
return false;
} finally {
socket.close();
}
return true;
}
}
@Override // Binder call
public boolean clearBcb() {
if (DEBUG) Slog.d(TAG, "clearBcb");
synchronized (sRequestLock) {
return setupOrClearBcb(false, null);
}
}
@Override // Binder call
public boolean setupBcb(String command) {
if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
synchronized (sRequestLock) {
return setupOrClearBcb(true, command);
}
}
@Override // Binder call
public void rebootRecoveryWithCommand(String command) {
if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
synchronized (sRequestLock) {
if (!setupOrClearBcb(true, command)) {
return;
}
// Having set up the BCB, go ahead and reboot.
PowerManager pm = mInjector.getPowerManager();
pm.reboot(PowerManager.REBOOT_RECOVERY);
}
}
@Override // Binder call
public boolean requestLskf(String updateToken, IntentSender intentSender) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
if (updateToken == null) {
return false;
}
// No need to prepare again for the same token.
if (mPreparedForReboot && updateToken.equals(mUnattendedRebootToken)) {
return true;
}
mPreparedForReboot = false;
mUnattendedRebootToken = updateToken;
mPreparedForRebootIntentSender = intentSender;
final long origId = Binder.clearCallingIdentity();
try {
mInjector.getLockSettingsService().prepareRebootEscrow();
} finally {
Binder.restoreCallingIdentity(origId);
}
return true;
}
@Override
public void onPreparedForReboot(boolean ready) {
if (mUnattendedRebootToken == null) {
Slog.w(TAG, "onPreparedForReboot called when mUnattendedRebootToken is null");
}
mPreparedForReboot = ready;
if (ready) {
sendPreparedForRebootIntentIfNeeded();
}
}
private void sendPreparedForRebootIntentIfNeeded() {
final IntentSender intentSender = mPreparedForRebootIntentSender;
if (intentSender != null) {
try {
intentSender.sendIntent(null, 0, null, null, null);
} catch (IntentSender.SendIntentException e) {
Slog.w(TAG, "Could not send intent for prepared reboot: " + e.getMessage());
}
}
}
@Override // Binder call
public boolean clearLskf() {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
mPreparedForReboot = false;
mUnattendedRebootToken = null;
mPreparedForRebootIntentSender = null;
final long origId = Binder.clearCallingIdentity();
try {
mInjector.getLockSettingsService().clearRebootEscrow();
} finally {
Binder.restoreCallingIdentity(origId);
}
return true;
}
@Override // Binder call
public boolean rebootWithLskf(String updateToken, String reason) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
if (!mPreparedForReboot) {
Slog.i(TAG, "Reboot requested before prepare completed");
return false;
}
if (updateToken != null && !updateToken.equals(mUnattendedRebootToken)) {
Slog.i(TAG, "Reboot requested after preparation, but with mismatching token");
return false;
}
if (!mInjector.getLockSettingsService().armRebootEscrow()) {
Slog.w(TAG, "Failure to escrow key for reboot");
return false;
}
PowerManager pm = mInjector.getPowerManager();
pm.reboot(reason);
return true;
}
/**
* Check if any of the init services is still running. If so, we cannot
* start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise
* it may break the socket communication since init creates / deletes
* the socket (/dev/socket/uncrypt) on service start / exit.
*/
private boolean checkAndWaitForUncryptService() {
for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
final String uncryptService = mInjector.systemPropertiesGet(INIT_SERVICE_UNCRYPT);
final String setupBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_SETUP_BCB);
final String clearBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_CLEAR_BCB);
final boolean busy = "running".equals(uncryptService)
|| "running".equals(setupBcbService) || "running".equals(clearBcbService);
if (DEBUG) {
Slog.i(TAG, "retry: " + retry + " busy: " + busy
+ " uncrypt: [" + uncryptService + "]"
+ " setupBcb: [" + setupBcbService + "]"
+ " clearBcb: [" + clearBcbService + "]");
}
if (!busy) {
return true;
}
try {
mInjector.threadSleep(1000);
} catch (InterruptedException e) {
Slog.w(TAG, "Interrupted:", e);
}
}
return false;
}
private boolean setupOrClearBcb(boolean isSetup, String command) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
final boolean available = checkAndWaitForUncryptService();
if (!available) {
Slog.e(TAG, "uncrypt service is unavailable.");
return false;
}
if (isSetup) {
mInjector.systemPropertiesSet("ctl.start", "setup-bcb");
} else {
mInjector.systemPropertiesSet("ctl.start", "clear-bcb");
}
// Connect to the uncrypt service socket.
UncryptSocket socket = mInjector.connectService();
if (socket == null) {
Slog.e(TAG, "Failed to connect to uncrypt socket");
return false;
}
try {
// Send the BCB commands if it's to setup BCB.
if (isSetup) {
socket.sendCommand(command);
}
// Read the status from the socket.
int status = socket.getPercentageUncrypted();
// Ack receipt of the status code. uncrypt waits for the ack so
// the socket won't be destroyed before we receive the code.
socket.sendAck();
if (status == 100) {
Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear")
+ " bcb successfully finished.");
} else {
// Error in /system/bin/uncrypt.
Slog.e(TAG, "uncrypt failed with status: " + status);
return false;
}
} catch (IOException e) {
Slog.e(TAG, "IOException when communicating with uncrypt:", e);
return false;
} finally {
socket.close();
}
return true;
}
/**
* Provides a wrapper for the low-level details of framing packets sent to the uncrypt
* socket.
*/
public static class UncryptSocket {
private LocalSocket mLocalSocket;
private DataInputStream mInputStream;
private DataOutputStream mOutputStream;
/**
* Attempt to connect to the uncrypt service. Connection will be retried for up to
* {@link #SOCKET_CONNECTION_MAX_RETRY} times. If the connection is unsuccessful, the
* socket will be closed. If the connection is successful, the connection must be closed
* by the caller.
*
* @return true if connection was successful, false if unsuccessful
*/
public boolean connectService() {
mLocalSocket = new LocalSocket();
boolean done = false;
// The uncrypt socket will be created by init upon receiving the
// service request. It may not be ready by this point. So we will
// keep retrying until success or reaching timeout.
for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
try {
mLocalSocket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
LocalSocketAddress.Namespace.RESERVED));
done = true;
break;
} catch (IOException ignored) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Slog.w(TAG, "Interrupted:", e);
}
}
}
if (!done) {
Slog.e(TAG, "Timed out connecting to uncrypt socket");
close();
return false;
}
try {
mInputStream = new DataInputStream(mLocalSocket.getInputStream());
mOutputStream = new DataOutputStream(mLocalSocket.getOutputStream());
} catch (IOException e) {
close();
return false;
}
return true;
}
/**
* Sends a command to the uncrypt service.
*
* @param command command to send to the uncrypt service
* @throws IOException if there was an error writing to the socket
*/
public void sendCommand(String command) throws IOException {
byte[] cmdUtf8 = command.getBytes(StandardCharsets.UTF_8);
mOutputStream.writeInt(cmdUtf8.length);
mOutputStream.write(cmdUtf8, 0, cmdUtf8.length);
}
/**
* Reads the status from the uncrypt service which is usually represented as a percentage.
* @return an integer representing the percentage completed
* @throws IOException if there was an error reading the socket
*/
public int getPercentageUncrypted() throws IOException {
return mInputStream.readInt();
}
/**
* Sends a confirmation to the uncrypt service.
* @throws IOException if there was an error writing to the socket
*/
public void sendAck() throws IOException {
mOutputStream.writeInt(0);
}
/**
* Closes the socket and all underlying data streams.
*/
public void close() {
IoUtils.closeQuietly(mInputStream);
IoUtils.closeQuietly(mOutputStream);
IoUtils.closeQuietly(mLocalSocket);
}
}
private boolean isCallerShell() {
final int callingUid = Binder.getCallingUid();
return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
}
private void enforceShell() {
if (!isCallerShell()) {
throw new SecurityException("Caller must be shell");
}
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
enforceShell();
final long origId = Binder.clearCallingIdentity();
try {
new RecoverySystemShellCommand(this).exec(
this, in, out, err, args, callback, resultReceiver);
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}