diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java new file mode 100644 index 0000000000000..130cfef99972d --- /dev/null +++ b/core/java/android/os/RecoverySystem.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2010 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.os; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import android.content.Context; +import android.util.Log; + +import org.apache.harmony.security.asn1.BerInputStream; +import org.apache.harmony.security.pkcs7.ContentInfo; +import org.apache.harmony.security.pkcs7.SignedData; +import org.apache.harmony.security.pkcs7.SignerInfo; +import org.apache.harmony.security.provider.cert.X509CertImpl; + +/** + * 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.) + * + * @pending + */ +public class RecoverySystem { + private static final String TAG = "RecoverySystem"; + + /** + * Default location of zip file containing public keys (X509 + * certs) authorized to sign OTA updates. + */ + private static final File DEFAULT_KEYSTORE = + new File("/system/etc/security/otacerts.zip"); + + /** Send progress to listeners no more often than this (in ms). */ + private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; + + /** Used to communicate with recovery. See bootable/recovery/recovery.c. */ + private static File RECOVERY_DIR = new File("/cache/recovery"); + private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); + private static File LOG_FILE = new File(RECOVERY_DIR, "log"); + + // Length limits for reading files. + private static int LOG_FILE_MAX_LENGTH = 8 * 1024; + + /** + * Interface definition for a callback to be invoked regularly as + * verification proceeds. + */ + public interface ProgressListener { + /** + * Called periodically as the verification progresses. + * + * @param progress the approximate percentage of the + * verification that has been completed, ranging from 0 + * to 100 (inclusive). + */ + public void onProgress(int progress); + } + + /** @return the set of certs that can be used to sign an OTA package. */ + private static HashSet getTrustedCerts(File keystore) + throws IOException, GeneralSecurityException { + HashSet trusted = new HashSet(); + if (keystore == null) { + keystore = DEFAULT_KEYSTORE; + } + ZipFile zip = new ZipFile(keystore); + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + trusted.add(cf.generateCertificate(zip.getInputStream(entry))); + } + } finally { + zip.close(); + } + return trusted; + } + + /** + * 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. + * + * @param packageFile the package to be verified + * @param listener an object to receive periodic progress + * updates as verification proceeds. May be null. + * @param deviceCertsZipFile the zip file of certificates whose + * public keys we will accept. Verification succeeds if the + * package is signed by the private key corresponding to any + * public key in this file. May be null to use the system default + * file (currently "/system/etc/security/otacerts.zip"). + * + * @throws IOException if there were any errors reading the + * package or certs files. + * @throws GeneralSecurityException if verification failed + */ + public static void verifyPackage(File packageFile, + ProgressListener listener, + File deviceCertsZipFile) + throws IOException, GeneralSecurityException { + 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); + Log.v(TAG, String.format("comment size %d; signature start %d", + commentSize, signatureStart)); + + 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"); + } + Collection 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 it = encCerts.iterator(); + X509Certificate cert = null; + if (it.hasNext()) { + cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next()); + } else { + throw new SignatureException("signature contains no certificates"); + } + + List 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 trusted = getTrustedCerts( + deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); + + PublicKey signatureKey = cert.getPublicKey(); + boolean verified = false; + for (Certificate 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 + // signatures. SignApk.java always uses SHA1withRSA, no + // matter what the cert says to use. Ignore + // cert.getSigAlgName(), and instead use whatever + // algorithm is used by the signature (which should be + // SHA1withRSA). + + 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]; + while (soFar < toRead) { + 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 (!sig.verify(sigInfo.getEncryptedDigest())) { + throw new SignatureException("signature digest verification failed"); + } + } finally { + raf.close(); + } + } + + /** + * Reboots the device in order to install the given update + * package. + * Requires the {@link android.Manifest.permission#REBOOT} + * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} + * permissions. + * + * @param context the Context to use + * @param packageFile the update package to install. Currently + * must be on the /cache or /data partitions. + * + * @throws IOException if writing the recovery command file + * fails, or if the reboot itself fails. + */ + public static void installPackage(Context context, File packageFile) + throws IOException { + String filename = packageFile.getCanonicalPath(); + + if (filename.startsWith("/cache/")) { + filename = "CACHE:" + filename.substring(7); + } else if (filename.startsWith("/data/")) { + filename = "DATA:" + filename.substring(6); + } else { + throw new IllegalArgumentException( + "Must start with /cache or /data: " + filename); + } + Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + String arg = "--update_package=" + filename; + bootCommand(context, arg); + } + + /** + * Reboots the device and wipes the user data partition. 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} + * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} + * permissions. + * + * @param context the Context to use + * + * @throws IOException if writing the recovery command file + * fails, or if the reboot itself fails. + */ + public static void rebootWipeUserData(Context context) + throws IOException { + bootCommand(context, "--wipe_data"); + } + + /** + * Reboot into the recovery system to wipe the /data partition and toggle + * Encrypted File Systems on/off. + * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. + * @throws IOException if something goes wrong. + * + * @hide + */ + public static void rebootToggleEFS(Context context, boolean efsEnabled) + throws IOException { + if (efsEnabled) { + bootCommand(context, "--set_encrypted_filesystem=on"); + } else { + bootCommand(context, "--set_encrypted_filesystem=off"); + } + } + + /** + * Reboot into the recovery system with the supplied argument. + * @param arg to pass to the recovery utility. + * @throws IOException if something goes wrong. + */ + private static void bootCommand(Context context, String arg) throws IOException { + 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 { + 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("recovery"); + + throw new IOException("Reboot failed (no permissions?)"); + } + + /** + * Called after booting to process and remove recovery-related files. + * @return the log file from recovery, or null if none was found. + * + * @hide + */ + public static String handleAftermath() { + // 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 + String[] names = RECOVERY_DIR.list(); + for (int i = 0; names != null && i < names.length; i++) { + 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; + } + + private void RecoverySystem() { } // Do not instantiate +} diff --git a/core/java/com/android/internal/os/RecoverySystem.java b/core/java/com/android/internal/os/RecoverySystem.java deleted file mode 100644 index 3aca683ebcc28..0000000000000 --- a/core/java/com/android/internal/os/RecoverySystem.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2008 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.internal.os; - -import android.os.FileUtils; -import android.os.Power; -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Iterator; -import java.util.Map; - -/** - * Utility class for interacting with the Android recovery partition. - * The recovery partition is a small standalone system which can perform - * operations that are difficult while the main system is running, like - * upgrading system software or reformatting the data partition. - * Note that most of these operations must be run as root. - * - * @hide - */ -public class RecoverySystem { - private static final String TAG = "RecoverySystem"; // for logging - - // Used to communicate with recovery. See commands/recovery/recovery.c. - private static File RECOVERY_DIR = new File("/cache/recovery"); - private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); - private static File LOG_FILE = new File(RECOVERY_DIR, "log"); - - // Length limits for reading files. - private static int LOG_FILE_MAX_LENGTH = 8 * 1024; - - /** - * Reboot into the recovery system to install a system update. - * @param update package to install (must be in /cache or /data). - * @throws IOException if something goes wrong. - */ - public static void rebootAndUpdate(File update) throws IOException { - String path = update.getCanonicalPath(); - if (path.startsWith("/cache/")) { - path = "CACHE:" + path.substring(7); - } else if (path.startsWith("/data/")) { - path = "DATA:" + path.substring(6); - } else { - throw new IllegalArgumentException( - "Must start with /cache or /data: " + path); - } - bootCommand("--update_package=" + path); - } - - /** - * Reboot into the recovery system to wipe the /data partition. - * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. - * @throws IOException if something goes wrong. - */ - public static void rebootAndWipe() throws IOException { - bootCommand("--wipe_data"); - } - - /** - * Reboot into the recovery system to wipe the /data partition and toggle - * Encrypted File Systems on/off. - * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. - * @throws IOException if something goes wrong. - * @hide - */ - public static void rebootAndToggleEFS(boolean efsEnabled) throws IOException { - if (efsEnabled) { - bootCommand("--set_encrypted_filesystem=on"); - } else { - bootCommand("--set_encrypted_filesystem=off"); - } - } - - /** - * Reboot into the recovery system with the supplied argument. - * @param arg to pass to the recovery utility. - * @throws IOException if something goes wrong. - */ - private static void bootCommand(String arg) throws IOException { - 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 { - command.write(arg); - command.write("\n"); - } finally { - command.close(); - } - - // Having written the command file, go ahead and reboot - Power.reboot("recovery"); - throw new IOException("Reboot failed (no permissions?)"); - } - - /** - * Called after booting to process and remove recovery-related files. - * @return the log file from recovery, or null if none was found. - */ - public static String handleAftermath() { - // 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 - String[] names = RECOVERY_DIR.list(); - for (int i = 0; names != null && i < names.length; i++) { - 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; - } -} diff --git a/services/java/com/android/server/BootReceiver.java b/services/java/com/android/server/BootReceiver.java index 84f0068093f67..4279e00701634 100644 --- a/services/java/com/android/server/BootReceiver.java +++ b/services/java/com/android/server/BootReceiver.java @@ -23,12 +23,11 @@ import android.content.Intent; import android.os.Build; import android.os.DropBoxManager; import android.os.FileUtils; +import android.os.RecoverySystem; import android.os.SystemProperties; import android.provider.Settings; import android.util.Log; -import com.android.internal.os.RecoverySystem; - import java.io.File; import java.io.IOException; diff --git a/services/java/com/android/server/FallbackCheckinService.java b/services/java/com/android/server/FallbackCheckinService.java index 1e5bc092d6cba..fc0ab6906d76e 100644 --- a/services/java/com/android/server/FallbackCheckinService.java +++ b/services/java/com/android/server/FallbackCheckinService.java @@ -21,11 +21,11 @@ import android.content.pm.PackageManager; import android.os.Binder; import android.os.ICheckinService; import android.os.IParentalControlCallback; +import android.os.RecoverySystem; import android.util.Log; import java.io.IOException; -import com.android.internal.os.RecoverySystem; import com.google.android.net.ParentalControlState; /** @@ -50,7 +50,7 @@ public final class FallbackCheckinService extends ICheckinService.Stub { // Save the android ID so the new system can get it erased. try { - RecoverySystem.rebootAndWipe(); + RecoverySystem.rebootWipeUserData(mContext); } catch (IOException e) { Log.e(TAG, "Reboot for masterClear() failed", e); } @@ -67,7 +67,7 @@ public final class FallbackCheckinService extends ICheckinService.Stub { // Save the android ID so the new system can get it erased. try { - RecoverySystem.rebootAndToggleEFS(efsEnabled); + RecoverySystem.rebootToggleEFS(mContext, efsEnabled); } catch (IOException e) { Log.e(TAG, "Reboot for toggle EFS failed", e); }