blob: d237fe7b4c3e6eb8044bc6e05e7edd4e34fde8b3 [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;
import android.content.Context;
import android.os.IRecoverySystem;
import android.os.IRecoverySystemProgressListener;
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 java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
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;
// A pipe file to monitor the uncrypt progress.
private static final String UNCRYPT_STATUS_FILE = "/cache/recovery/uncrypt_status";
// Temporary command file to communicate between the system server and uncrypt.
private static final String COMMAND_FILE = "/cache/recovery/command";
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);
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
// 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.getMessage());
return false;
}
// Create the status pipe file to communicate with uncrypt.
new File(UNCRYPT_STATUS_FILE).delete();
try {
Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
} catch (ErrnoException e) {
Slog.e(TAG, "ErrnoException when creating named pipe \"" + UNCRYPT_STATUS_FILE +
"\": " + e.getMessage());
return false;
}
// Trigger uncrypt via init.
SystemProperties.set("ctl.start", "uncrypt");
// Read the status from the pipe.
try (BufferedReader reader = new BufferedReader(new FileReader(UNCRYPT_STATUS_FILE))) {
int lastStatus = Integer.MIN_VALUE;
while (true) {
String str = reader.readLine();
try {
int status = Integer.parseInt(str);
// 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 unused) {
Slog.w(TAG, "RemoteException when posting progress");
}
}
if (status == 100) {
Slog.i(TAG, "uncrypt successfully finished.");
break;
}
} else {
// Error in /system/bin/uncrypt.
Slog.e(TAG, "uncrypt failed with status: " + status);
return false;
}
} catch (NumberFormatException unused) {
Slog.e(TAG, "uncrypt invalid status received: " + str);
return false;
}
}
} catch (IOException unused) {
Slog.e(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
return false;
}
return true;
}
@Override // Binder call
public boolean clearBcb() {
if (DEBUG) Slog.d(TAG, "clearBcb");
return setupOrClearBcb(false, null);
}
@Override // Binder call
public boolean setupBcb(String command) {
if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
return setupOrClearBcb(true, command);
}
private boolean setupOrClearBcb(boolean isSetup, String command) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
if (isSetup) {
// Set up the command file to be read by uncrypt.
try (FileWriter commandFile = new FileWriter(COMMAND_FILE)) {
commandFile.write(command + "\n");
} catch (IOException e) {
Slog.e(TAG, "IOException when writing \"" + COMMAND_FILE +
"\": " + e.getMessage());
return false;
}
}
// Create the status pipe file to communicate with uncrypt.
new File(UNCRYPT_STATUS_FILE).delete();
try {
Os.mkfifo(UNCRYPT_STATUS_FILE, 0600);
} catch (ErrnoException e) {
Slog.e(TAG, "ErrnoException when creating named pipe \"" +
UNCRYPT_STATUS_FILE + "\": " + e.getMessage());
return false;
}
if (isSetup) {
SystemProperties.set("ctl.start", "setup-bcb");
} else {
SystemProperties.set("ctl.start", "clear-bcb");
}
// Read the status from the pipe.
try (BufferedReader reader = new BufferedReader(new FileReader(UNCRYPT_STATUS_FILE))) {
while (true) {
String str = reader.readLine();
try {
int status = Integer.parseInt(str);
if (status == 100) {
Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear") +
" bcb successfully finished.");
break;
} else {
// Error in /system/bin/uncrypt.
Slog.e(TAG, "uncrypt failed with status: " + status);
return false;
}
} catch (NumberFormatException unused) {
Slog.e(TAG, "uncrypt invalid status received: " + str);
return false;
}
}
} catch (IOException unused) {
Slog.e(TAG, "IOException when reading \"" + UNCRYPT_STATUS_FILE + "\".");
return false;
}
// Delete the command file as we don't need it anymore.
new File(COMMAND_FILE).delete();
return true;
}
}
}