content: /*
   * 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;
  
  import android.content.Context;
  import android.net.LocalSocket;
  import android.net.LocalSocketAddress;
  import android.os.IRecoverySystem;
  import android.os.IRecoverySystemProgressListener;
  import android.os.PowerManager;
  import android.os.RecoverySystem;
  import android.os.RemoteException;
  import android.os.SystemProperties;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.util.Slog;
  
  import libcore.io.IoUtils;
  
  import java.io.DataInputStream;
  import java.io.DataOutputStream;
  import java.io.File;
  import java.io.FileWriter;
  import java.io.IOException;
  
  /**
   * 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 final class RecoverySystemService extends SystemService {
      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.
      private static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
      private static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
      private static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
  
      private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
  
      private static final Object sRequestLock = new Object();
  
      private Context mContext;
  
      public RecoverySystemService(Context context) {
          super(context);
          mContext = context;
      }
  
      @Override
      public void onStart() {
          publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
      }
  
      private final class BinderService extends IRecoverySystem.Stub {
          @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);
  
                  final boolean available = checkAndWaitForUncryptService();
                  if (!available) {
                      Slog.e(TAG, "uncrypt service is unavailable.");
                      return false;
                  }
  
                  // Write the filename into UNCRYPT_PACKAGE_FILE to be read by
                  // uncrypt.
                  RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
  
                  try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) {
                      uncryptFile.write(filename + "\n");
                  } catch (IOException e) {
                      Slog.e(TAG, "IOException when writing \"" +
                              RecoverySystem.UNCRYPT_PACKAGE_FILE + "\":", e);
                      return false;
                  }
  
                  // Trigger uncrypt via init.
                  SystemProperties.set("ctl.start", "uncrypt");
  
                  // Connect to the uncrypt service socket.
                  LocalSocket socket = connectService();
                  if (socket == null) {
                      Slog.e(TAG, "Failed to connect to uncrypt socket");
                      return false;
                  }
  
                  // Read the status from the socket.
                  DataInputStream dis = null;
                  DataOutputStream dos = null;
                  try {
                      dis = new DataInputStream(socket.getInputStream());
                      dos = new DataOutputStream(socket.getOutputStream());
                      int lastStatus = Integer.MIN_VALUE;
                      while (true) {
                          int status = dis.readInt();
                          // 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.
                                  dos.writeInt(0);
                                  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.
                              dos.writeInt(0);
                              return false;
                          }
                      }
                  } catch (IOException e) {
                      Slog.e(TAG, "IOException when reading status: ", e);
                      return false;
                  } finally {
                      IoUtils.closeQuietly(dis);
                      IoUtils.closeQuietly(dos);
                      IoUtils.closeQuietly(socket);
                  }
  
                  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 = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
                  pm.reboot(PowerManager.REBOOT_RECOVERY);
              }
          }
  
          /**
           * 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 = SystemProperties.get(INIT_SERVICE_UNCRYPT);
                  final String setupBcbService = SystemProperties.get(INIT_SERVICE_SETUP_BCB);
                  final String clearBcbService = SystemProperties.get(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 {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      Slog.w(TAG, "Interrupted:", e);
                  }
              }
  
              return false;
          }
  
          private LocalSocket connectService() {
              LocalSocket socket = 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 {
                      socket.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");
                  return null;
              }
              return socket;
          }
  
          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) {
                  SystemProperties.set("ctl.start", "setup-bcb");
              } else {
                  SystemProperties.set("ctl.start", "clear-bcb");
              }
  
              // Connect to the uncrypt service socket.
              LocalSocket socket = connectService();
              if (socket == null) {
                  Slog.e(TAG, "Failed to connect to uncrypt socket");
                  return false;
              }
  
              DataInputStream dis = null;
              DataOutputStream dos = null;
              try {
                  dis = new DataInputStream(socket.getInputStream());
                  dos = new DataOutputStream(socket.getOutputStream());
  
                  // Send the BCB commands if it's to setup BCB.
                  if (isSetup) {
                      byte[] cmdUtf8 = command.getBytes("UTF-8");
                      dos.writeInt(cmdUtf8.length);
                      dos.write(cmdUtf8, 0, cmdUtf8.length);
                      dos.flush();
                  }
  
                  // Read the status from the socket.
                  int status = dis.readInt();
  
                  // Ack receipt of the status code. uncrypt waits for the ack so
                  // the socket won't be destroyed before we receive the code.
                  dos.writeInt(0);
  
                  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 {
                  IoUtils.closeQuietly(dis);
                  IoUtils.closeQuietly(dos);
                  IoUtils.closeQuietly(socket);
              }
  
              return true;
          }
      }
  }
  