RecoverySystem.java revision e8217ff4a725004e495ed1506928334f97e5bbf1
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.os; 18 19import android.content.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.os.UserManager; 23import android.text.TextUtils; 24import android.util.Log; 25 26import java.io.ByteArrayInputStream; 27import java.io.File; 28import java.io.FileNotFoundException; 29import java.io.FileWriter; 30import java.io.IOException; 31import java.io.InputStream; 32import java.io.RandomAccessFile; 33import java.security.GeneralSecurityException; 34import java.security.PublicKey; 35import java.security.Signature; 36import java.security.SignatureException; 37import java.security.cert.CertificateFactory; 38import java.security.cert.X509Certificate; 39import java.util.Enumeration; 40import java.util.HashSet; 41import java.util.Iterator; 42import java.util.List; 43import java.util.Locale; 44import java.util.zip.ZipEntry; 45import java.util.zip.ZipFile; 46 47import sun.security.pkcs.PKCS7; 48import sun.security.pkcs.SignerInfo; 49 50/** 51 * RecoverySystem contains methods for interacting with the Android 52 * recovery system (the separate partition that can be used to install 53 * system updates, wipe user data, etc.) 54 */ 55public class RecoverySystem { 56 private static final String TAG = "RecoverySystem"; 57 58 /** 59 * Default location of zip file containing public keys (X509 60 * certs) authorized to sign OTA updates. 61 */ 62 private static final File DEFAULT_KEYSTORE = 63 new File("/system/etc/security/otacerts.zip"); 64 65 /** Send progress to listeners no more often than this (in ms). */ 66 private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; 67 68 /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ 69 private static File RECOVERY_DIR = new File("/cache/recovery"); 70 private static File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map"); 71 private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); 72 private static File UNCRYPT_FILE = new File(RECOVERY_DIR, "uncrypt_file"); 73 private static File LOG_FILE = new File(RECOVERY_DIR, "log"); 74 private static String LAST_PREFIX = "last_"; 75 76 // Length limits for reading files. 77 private static int LOG_FILE_MAX_LENGTH = 64 * 1024; 78 79 /** 80 * Interface definition for a callback to be invoked regularly as 81 * verification proceeds. 82 */ 83 public interface ProgressListener { 84 /** 85 * Called periodically as the verification progresses. 86 * 87 * @param progress the approximate percentage of the 88 * verification that has been completed, ranging from 0 89 * to 100 (inclusive). 90 */ 91 public void onProgress(int progress); 92 } 93 94 /** @return the set of certs that can be used to sign an OTA package. */ 95 private static HashSet<X509Certificate> getTrustedCerts(File keystore) 96 throws IOException, GeneralSecurityException { 97 HashSet<X509Certificate> trusted = new HashSet<X509Certificate>(); 98 if (keystore == null) { 99 keystore = DEFAULT_KEYSTORE; 100 } 101 ZipFile zip = new ZipFile(keystore); 102 try { 103 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 104 Enumeration<? extends ZipEntry> entries = zip.entries(); 105 while (entries.hasMoreElements()) { 106 ZipEntry entry = entries.nextElement(); 107 InputStream is = zip.getInputStream(entry); 108 try { 109 trusted.add((X509Certificate) cf.generateCertificate(is)); 110 } finally { 111 is.close(); 112 } 113 } 114 } finally { 115 zip.close(); 116 } 117 return trusted; 118 } 119 120 /** 121 * Verify the cryptographic signature of a system update package 122 * before installing it. Note that the package is also verified 123 * separately by the installer once the device is rebooted into 124 * the recovery system. This function will return only if the 125 * package was successfully verified; otherwise it will throw an 126 * exception. 127 * 128 * Verification of a package can take significant time, so this 129 * function should not be called from a UI thread. Interrupting 130 * the thread while this function is in progress will result in a 131 * SecurityException being thrown (and the thread's interrupt flag 132 * will be cleared). 133 * 134 * @param packageFile the package to be verified 135 * @param listener an object to receive periodic progress 136 * updates as verification proceeds. May be null. 137 * @param deviceCertsZipFile the zip file of certificates whose 138 * public keys we will accept. Verification succeeds if the 139 * package is signed by the private key corresponding to any 140 * public key in this file. May be null to use the system default 141 * file (currently "/system/etc/security/otacerts.zip"). 142 * 143 * @throws IOException if there were any errors reading the 144 * package or certs files. 145 * @throws GeneralSecurityException if verification failed 146 */ 147 public static void verifyPackage(File packageFile, 148 ProgressListener listener, 149 File deviceCertsZipFile) 150 throws IOException, GeneralSecurityException { 151 final long fileLen = packageFile.length(); 152 153 final RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); 154 try { 155 final long startTimeMillis = System.currentTimeMillis(); 156 if (listener != null) { 157 listener.onProgress(0); 158 } 159 160 raf.seek(fileLen - 6); 161 byte[] footer = new byte[6]; 162 raf.readFully(footer); 163 164 if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { 165 throw new SignatureException("no signature in file (no footer)"); 166 } 167 168 final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); 169 final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); 170 171 byte[] eocd = new byte[commentSize + 22]; 172 raf.seek(fileLen - (commentSize + 22)); 173 raf.readFully(eocd); 174 175 // Check that we have found the start of the 176 // end-of-central-directory record. 177 if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || 178 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { 179 throw new SignatureException("no signature in file (bad footer)"); 180 } 181 182 for (int i = 4; i < eocd.length-3; ++i) { 183 if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && 184 eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { 185 throw new SignatureException("EOCD marker found after start of EOCD"); 186 } 187 } 188 189 // Parse the signature 190 PKCS7 block = 191 new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); 192 193 // Take the first certificate from the signature (packages 194 // should contain only one). 195 X509Certificate[] certificates = block.getCertificates(); 196 if (certificates == null || certificates.length == 0) { 197 throw new SignatureException("signature contains no certificates"); 198 } 199 X509Certificate cert = certificates[0]; 200 PublicKey signatureKey = cert.getPublicKey(); 201 202 SignerInfo[] signerInfos = block.getSignerInfos(); 203 if (signerInfos == null || signerInfos.length == 0) { 204 throw new SignatureException("signature contains no signedData"); 205 } 206 SignerInfo signerInfo = signerInfos[0]; 207 208 // Check that the public key of the certificate contained 209 // in the package equals one of our trusted public keys. 210 boolean verified = false; 211 HashSet<X509Certificate> trusted = getTrustedCerts( 212 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); 213 for (X509Certificate c : trusted) { 214 if (c.getPublicKey().equals(signatureKey)) { 215 verified = true; 216 break; 217 } 218 } 219 if (!verified) { 220 throw new SignatureException("signature doesn't match any trusted key"); 221 } 222 223 // The signature cert matches a trusted key. Now verify that 224 // the digest in the cert matches the actual file data. 225 raf.seek(0); 226 final ProgressListener listenerForInner = listener; 227 SignerInfo verifyResult = block.verify(signerInfo, new InputStream() { 228 // The signature covers all of the OTA package except the 229 // archive comment and its 2-byte length. 230 long toRead = fileLen - commentSize - 2; 231 long soFar = 0; 232 233 int lastPercent = 0; 234 long lastPublishTime = startTimeMillis; 235 236 @Override 237 public int read() throws IOException { 238 throw new UnsupportedOperationException(); 239 } 240 241 @Override 242 public int read(byte[] b, int off, int len) throws IOException { 243 if (soFar >= toRead) { 244 return -1; 245 } 246 if (Thread.currentThread().isInterrupted()) { 247 return -1; 248 } 249 250 int size = len; 251 if (soFar + size > toRead) { 252 size = (int)(toRead - soFar); 253 } 254 int read = raf.read(b, off, size); 255 soFar += read; 256 257 if (listenerForInner != null) { 258 long now = System.currentTimeMillis(); 259 int p = (int)(soFar * 100 / toRead); 260 if (p > lastPercent && 261 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 262 lastPercent = p; 263 lastPublishTime = now; 264 listenerForInner.onProgress(lastPercent); 265 } 266 } 267 268 return read; 269 } 270 }); 271 272 final boolean interrupted = Thread.interrupted(); 273 if (listener != null) { 274 listener.onProgress(100); 275 } 276 277 if (interrupted) { 278 throw new SignatureException("verification was interrupted"); 279 } 280 281 if (verifyResult == null) { 282 throw new SignatureException("signature digest verification failed"); 283 } 284 } finally { 285 raf.close(); 286 } 287 } 288 289 /** 290 * Reboots the device in order to install the given update 291 * package. 292 * Requires the {@link android.Manifest.permission#REBOOT} permission. 293 * 294 * @param context the Context to use 295 * @param packageFile the update package to install. Must be on 296 * a partition mountable by recovery. (The set of partitions 297 * known to recovery may vary from device to device. Generally, 298 * /cache and /data are safe.) 299 * 300 * @throws IOException if writing the recovery command file 301 * fails, or if the reboot itself fails. 302 */ 303 public static void installPackage(Context context, File packageFile) 304 throws IOException { 305 String filename = packageFile.getCanonicalPath(); 306 307 FileWriter uncryptFile = new FileWriter(UNCRYPT_FILE); 308 try { 309 uncryptFile.write(filename + "\n"); 310 } finally { 311 uncryptFile.close(); 312 } 313 // UNCRYPT_FILE needs to be readable by system server on bootup. 314 if (!UNCRYPT_FILE.setReadable(true, false)) { 315 Log.e(TAG, "Error setting readable for " + UNCRYPT_FILE.getCanonicalPath()); 316 } 317 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); 318 319 // If the package is on the /data partition, write the block map file 320 // into COMMAND_FILE instead. 321 if (filename.startsWith("/data/")) { 322 filename = "@/cache/recovery/block.map"; 323 } 324 325 final String filenameArg = "--update_package=" + filename; 326 final String localeArg = "--locale=" + Locale.getDefault().toString(); 327 bootCommand(context, filenameArg, localeArg); 328 } 329 330 /** 331 * Reboots the device and wipes the user data and cache 332 * partitions. This is sometimes called a "factory reset", which 333 * is something of a misnomer because the system partition is not 334 * restored to its factory state. Requires the 335 * {@link android.Manifest.permission#REBOOT} permission. 336 * 337 * @param context the Context to use 338 * 339 * @throws IOException if writing the recovery command file 340 * fails, or if the reboot itself fails. 341 * @throws SecurityException if the current user is not allowed to wipe data. 342 */ 343 public static void rebootWipeUserData(Context context) throws IOException { 344 rebootWipeUserData(context, false, context.getPackageName()); 345 } 346 347 /** {@hide} */ 348 public static void rebootWipeUserData(Context context, String reason) throws IOException { 349 rebootWipeUserData(context, false, reason); 350 } 351 352 /** {@hide} */ 353 public static void rebootWipeUserData(Context context, boolean shutdown) 354 throws IOException { 355 rebootWipeUserData(context, shutdown, context.getPackageName()); 356 } 357 358 /** 359 * Reboots the device and wipes the user data and cache 360 * partitions. This is sometimes called a "factory reset", which 361 * is something of a misnomer because the system partition is not 362 * restored to its factory state. Requires the 363 * {@link android.Manifest.permission#REBOOT} permission. 364 * 365 * @param context the Context to use 366 * @param shutdown if true, the device will be powered down after 367 * the wipe completes, rather than being rebooted 368 * back to the regular system. 369 * 370 * @throws IOException if writing the recovery command file 371 * fails, or if the reboot itself fails. 372 * @throws SecurityException if the current user is not allowed to wipe data. 373 * 374 * @hide 375 */ 376 public static void rebootWipeUserData(Context context, boolean shutdown, String reason) 377 throws IOException { 378 UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); 379 if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) { 380 throw new SecurityException("Wiping data is not allowed for this user."); 381 } 382 final ConditionVariable condition = new ConditionVariable(); 383 384 Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION"); 385 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 386 context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM, 387 android.Manifest.permission.MASTER_CLEAR, 388 new BroadcastReceiver() { 389 @Override 390 public void onReceive(Context context, Intent intent) { 391 condition.open(); 392 } 393 }, null, 0, null, null); 394 395 // Block until the ordered broadcast has completed. 396 condition.block(); 397 398 String shutdownArg = null; 399 if (shutdown) { 400 shutdownArg = "--shutdown_after"; 401 } 402 403 String reasonArg = null; 404 if (!TextUtils.isEmpty(reason)) { 405 reasonArg = "--reason=" + sanitizeArg(reason); 406 } 407 408 final String localeArg = "--locale=" + Locale.getDefault().toString(); 409 bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg); 410 } 411 412 /** 413 * Reboot into the recovery system to wipe the /cache partition. 414 * @throws IOException if something goes wrong. 415 */ 416 public static void rebootWipeCache(Context context) throws IOException { 417 rebootWipeCache(context, context.getPackageName()); 418 } 419 420 /** {@hide} */ 421 public static void rebootWipeCache(Context context, String reason) throws IOException { 422 String reasonArg = null; 423 if (!TextUtils.isEmpty(reason)) { 424 reasonArg = "--reason=" + sanitizeArg(reason); 425 } 426 427 final String localeArg = "--locale=" + Locale.getDefault().toString(); 428 bootCommand(context, "--wipe_cache", reasonArg, localeArg); 429 } 430 431 /** 432 * Reboot into the recovery system with the supplied argument. 433 * @param args to pass to the recovery utility. 434 * @throws IOException if something goes wrong. 435 */ 436 private static void bootCommand(Context context, String... args) throws IOException { 437 RECOVERY_DIR.mkdirs(); // In case we need it 438 COMMAND_FILE.delete(); // In case it's not writable 439 LOG_FILE.delete(); 440 441 FileWriter command = new FileWriter(COMMAND_FILE); 442 try { 443 for (String arg : args) { 444 if (!TextUtils.isEmpty(arg)) { 445 command.write(arg); 446 command.write("\n"); 447 } 448 } 449 } finally { 450 command.close(); 451 } 452 453 // Having written the command file, go ahead and reboot 454 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 455 pm.reboot(PowerManager.REBOOT_RECOVERY); 456 457 throw new IOException("Reboot failed (no permissions?)"); 458 } 459 460 /** 461 * Called after booting to process and remove recovery-related files. 462 * @return the log file from recovery, or null if none was found. 463 * 464 * @hide 465 */ 466 public static String handleAftermath() { 467 // Record the tail of the LOG_FILE 468 String log = null; 469 try { 470 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); 471 } catch (FileNotFoundException e) { 472 Log.i(TAG, "No recovery log file"); 473 } catch (IOException e) { 474 Log.e(TAG, "Error reading recovery log", e); 475 } 476 477 // Only remove the OTA package if it's partially processed (uncrypt'd). 478 boolean reservePackage = BLOCK_MAP_FILE.exists(); 479 if (!reservePackage && UNCRYPT_FILE.exists()) { 480 String filename = null; 481 try { 482 filename = FileUtils.readTextFile(UNCRYPT_FILE, 0, null); 483 } catch (IOException e) { 484 Log.e(TAG, "Error reading uncrypt file", e); 485 } 486 487 // Remove the OTA package on /data that has been (possibly 488 // partially) processed. (Bug: 24973532) 489 if (filename != null && filename.startsWith("/data")) { 490 if (UNCRYPT_FILE.delete()) { 491 Log.i(TAG, "Deleted: " + filename); 492 } else { 493 Log.e(TAG, "Can't delete: " + filename); 494 } 495 } 496 } 497 498 // We keep the update logs (beginning with LAST_PREFIX), and optionally 499 // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE 500 // will be created at the end of a successful uncrypt. If seeing this 501 // file, we keep the block map file and the file that contains the 502 // package name (UNCRYPT_FILE). This is to reduce the work for GmsCore 503 // to avoid re-downloading everything again. 504 String[] names = RECOVERY_DIR.list(); 505 for (int i = 0; names != null && i < names.length; i++) { 506 if (names[i].startsWith(LAST_PREFIX)) continue; 507 if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue; 508 if (reservePackage && names[i].equals(UNCRYPT_FILE.getName())) continue; 509 510 recursiveDelete(new File(RECOVERY_DIR, names[i])); 511 } 512 513 return log; 514 } 515 516 /** 517 * Internally, delete a given file or directory recursively. 518 */ 519 private static void recursiveDelete(File name) { 520 if (name.isDirectory()) { 521 String[] files = name.list(); 522 for (int i = 0; files != null && i < files.length; i++) { 523 File f = new File(name, files[i]); 524 recursiveDelete(f); 525 } 526 } 527 528 if (!name.delete()) { 529 Log.e(TAG, "Can't delete: " + name); 530 } else { 531 Log.i(TAG, "Deleted: " + name); 532 } 533 } 534 535 /** 536 * Internally, recovery treats each line of the command file as a separate 537 * argv, so we only need to protect against newlines and nulls. 538 */ 539 private static String sanitizeArg(String arg) { 540 arg = arg.replace('\0', '?'); 541 arg = arg.replace('\n', '?'); 542 return arg; 543 } 544 545 546 /** 547 * @removed Was previously made visible by accident. 548 */ 549 public RecoverySystem() { } 550} 551