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