RecoverySystempublic class RecoverySystem extends Object RecoverySystem contains methods for interacting with the Android
recovery system (the separate partition that can be used to install
system updates, wipe user data, etc.) |
Fields Summary |
---|
private static final String | TAG | private static final File | DEFAULT_KEYSTOREDefault location of zip file containing public keys (X509
certs) authorized to sign OTA updates. | private static final long | PUBLISH_PROGRESS_INTERVAL_MSSend progress to listeners no more often than this (in ms). | private static File | RECOVERY_DIRUsed to communicate with recovery. See bootable/recovery/recovery.c. | private static File | COMMAND_FILE | private static File | LOG_FILE | private static String | LAST_PREFIX | private static int | LOG_FILE_MAX_LENGTH |
Methods Summary |
---|
private void | RecoverySystem()
| private static void | bootCommand(android.content.Context context, java.lang.String args)Reboot into the recovery system with the supplied argument.
RECOVERY_DIR.mkdirs(); // In case we need it
COMMAND_FILE.delete(); // In case it's not writable
LOG_FILE.delete();
FileWriter command = new FileWriter(COMMAND_FILE);
try {
for (String arg : args) {
if (!TextUtils.isEmpty(arg)) {
command.write(arg);
command.write("\n");
}
}
} finally {
command.close();
}
// Having written the command file, go ahead and reboot
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
pm.reboot(PowerManager.REBOOT_RECOVERY);
throw new IOException("Reboot failed (no permissions?)");
| private static java.util.HashSet | getTrustedCerts(java.io.File keystore)
HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
if (keystore == null) {
keystore = DEFAULT_KEYSTORE;
}
ZipFile zip = new ZipFile(keystore);
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
InputStream is = zip.getInputStream(entry);
try {
trusted.add((X509Certificate) cf.generateCertificate(is));
} finally {
is.close();
}
}
} finally {
zip.close();
}
return trusted;
| public static java.lang.String | handleAftermath()Called after booting to process and remove recovery-related files.
// Record the tail of the LOG_FILE
String log = null;
try {
log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
} catch (FileNotFoundException e) {
Log.i(TAG, "No recovery log file");
} catch (IOException e) {
Log.e(TAG, "Error reading recovery log", e);
}
// Delete everything in RECOVERY_DIR except those beginning
// with LAST_PREFIX
String[] names = RECOVERY_DIR.list();
for (int i = 0; names != null && i < names.length; i++) {
if (names[i].startsWith(LAST_PREFIX)) continue;
File f = new File(RECOVERY_DIR, names[i]);
if (!f.delete()) {
Log.e(TAG, "Can't delete: " + f);
} else {
Log.i(TAG, "Deleted: " + f);
}
}
return log;
| public static void | installPackage(android.content.Context context, java.io.File packageFile)Reboots the device in order to install the given update
package.
Requires the {@link android.Manifest.permission#REBOOT} permission.
String filename = packageFile.getCanonicalPath();
Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
final String filenameArg = "--update_package=" + filename;
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, filenameArg, localeArg);
| public static void | rebootWipeCache(android.content.Context context)Reboot into the recovery system to wipe the /cache partition.
rebootWipeCache(context, context.getPackageName());
| public static void | rebootWipeCache(android.content.Context context, java.lang.String reason){@hide}
String reasonArg = null;
if (!TextUtils.isEmpty(reason)) {
reasonArg = "--reason=" + sanitizeArg(reason);
}
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, "--wipe_cache", reasonArg, localeArg);
| public static void | rebootWipeUserData(android.content.Context context)Reboots the device and wipes the user data and cache
partitions. This is sometimes called a "factory reset", which
is something of a misnomer because the system partition is not
restored to its factory state. Requires the
{@link android.Manifest.permission#REBOOT} permission.
rebootWipeUserData(context, false, context.getPackageName());
| public static void | rebootWipeUserData(android.content.Context context, java.lang.String reason){@hide}
rebootWipeUserData(context, false, reason);
| public static void | rebootWipeUserData(android.content.Context context, boolean shutdown){@hide}
rebootWipeUserData(context, shutdown, context.getPackageName());
| public static void | rebootWipeUserData(android.content.Context context, boolean shutdown, java.lang.String reason)Reboots the device and wipes the user data and cache
partitions. This is sometimes called a "factory reset", which
is something of a misnomer because the system partition is not
restored to its factory state. Requires the
{@link android.Manifest.permission#REBOOT} permission.
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
throw new SecurityException("Wiping data is not allowed for this user.");
}
final ConditionVariable condition = new ConditionVariable();
Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
android.Manifest.permission.MASTER_CLEAR,
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
condition.open();
}
}, null, 0, null, null);
// Block until the ordered broadcast has completed.
condition.block();
String shutdownArg = null;
if (shutdown) {
shutdownArg = "--shutdown_after";
}
String reasonArg = null;
if (!TextUtils.isEmpty(reason)) {
reasonArg = "--reason=" + sanitizeArg(reason);
}
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
| private static java.lang.String | sanitizeArg(java.lang.String arg)Internally, recovery treats each line of the command file as a separate
argv, so we only need to protect against newlines and nulls.
arg = arg.replace('\0", '?");
arg = arg.replace('\n", '?");
return arg;
| public static void | verifyPackage(java.io.File packageFile, android.os.RecoverySystem$ProgressListener listener, java.io.File deviceCertsZipFile)Verify the cryptographic signature of a system update package
before installing it. Note that the package is also verified
separately by the installer once the device is rebooted into
the recovery system. This function will return only if the
package was successfully verified; otherwise it will throw an
exception.
Verification of a package can take significant time, so this
function should not be called from a UI thread. Interrupting
the thread while this function is in progress will result in a
SecurityException being thrown (and the thread's interrupt flag
will be cleared).
long fileLen = packageFile.length();
RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
try {
int lastPercent = 0;
long lastPublishTime = System.currentTimeMillis();
if (listener != null) {
listener.onProgress(lastPercent);
}
raf.seek(fileLen - 6);
byte[] footer = new byte[6];
raf.readFully(footer);
if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
throw new SignatureException("no signature in file (no footer)");
}
int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
byte[] eocd = new byte[commentSize + 22];
raf.seek(fileLen - (commentSize + 22));
raf.readFully(eocd);
// Check that we have found the start of the
// end-of-central-directory record.
if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
throw new SignatureException("no signature in file (bad footer)");
}
for (int i = 4; i < eocd.length-3; ++i) {
if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
throw new SignatureException("EOCD marker found after start of EOCD");
}
}
// The following code is largely copied from
// JarUtils.verifySignature(). We could just *call* that
// method here if that function didn't read the entire
// input (ie, the whole OTA package) into memory just to
// compute its message digest.
BerInputStream bis = new BerInputStream(
new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
SignedData signedData = info.getSignedData();
if (signedData == null) {
throw new IOException("signedData is null");
}
List<Certificate> encCerts = signedData.getCertificates();
if (encCerts.isEmpty()) {
throw new IOException("encCerts is empty");
}
// Take the first certificate from the signature (packages
// should contain only one).
Iterator<Certificate> it = encCerts.iterator();
X509Certificate cert = null;
if (it.hasNext()) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream is = new ByteArrayInputStream(it.next().getEncoded());
cert = (X509Certificate) cf.generateCertificate(is);
} else {
throw new SignatureException("signature contains no certificates");
}
List<SignerInfo> sigInfos = signedData.getSignerInfos();
SignerInfo sigInfo;
if (!sigInfos.isEmpty()) {
sigInfo = (SignerInfo)sigInfos.get(0);
} else {
throw new IOException("no signer infos!");
}
// Check that the public key of the certificate contained
// in the package equals one of our trusted public keys.
HashSet<X509Certificate> trusted = getTrustedCerts(
deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
PublicKey signatureKey = cert.getPublicKey();
boolean verified = false;
for (X509Certificate c : trusted) {
if (c.getPublicKey().equals(signatureKey)) {
verified = true;
break;
}
}
if (!verified) {
throw new SignatureException("signature doesn't match any trusted key");
}
// The signature cert matches a trusted key. Now verify that
// the digest in the cert matches the actual file data.
// The verifier in recovery only handles SHA1withRSA and
// SHA256withRSA signatures. SignApk chooses which to use
// based on the signature algorithm of the cert:
//
// "SHA256withRSA" cert -> "SHA256withRSA" signature
// "SHA1withRSA" cert -> "SHA1withRSA" signature
// "MD5withRSA" cert -> "SHA1withRSA" signature (for backwards compatibility)
// any other cert -> SignApk fails
//
// Here we ignore whatever the cert says, and instead use
// whatever algorithm is used by the signature.
String da = sigInfo.getDigestAlgorithm();
String dea = sigInfo.getDigestEncryptionAlgorithm();
String alg = null;
if (da == null || dea == null) {
// fall back to the cert algorithm if the sig one
// doesn't look right.
alg = cert.getSigAlgName();
} else {
alg = da + "with" + dea;
}
Signature sig = Signature.getInstance(alg);
sig.initVerify(cert);
// The signature covers all of the OTA package except the
// archive comment and its 2-byte length.
long toRead = fileLen - commentSize - 2;
long soFar = 0;
raf.seek(0);
byte[] buffer = new byte[4096];
boolean interrupted = false;
while (soFar < toRead) {
interrupted = Thread.interrupted();
if (interrupted) break;
int size = buffer.length;
if (soFar + size > toRead) {
size = (int)(toRead - soFar);
}
int read = raf.read(buffer, 0, size);
sig.update(buffer, 0, read);
soFar += read;
if (listener != null) {
long now = System.currentTimeMillis();
int p = (int)(soFar * 100 / toRead);
if (p > lastPercent &&
now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
lastPercent = p;
lastPublishTime = now;
listener.onProgress(lastPercent);
}
}
}
if (listener != null) {
listener.onProgress(100);
}
if (interrupted) {
throw new SignatureException("verification was interrupted");
}
if (!sig.verify(sigInfo.getEncryptedDigest())) {
throw new SignatureException("signature digest verification failed");
}
} finally {
raf.close();
}
|
|