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