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