RecoverySystem.java revision fe0538098403b49ebd9219bf77236471bb5ca63b
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.util.Log; 24 25import java.io.ByteArrayInputStream; 26import java.io.File; 27import java.io.FileNotFoundException; 28import java.io.FileWriter; 29import java.io.IOException; 30import java.io.InputStream; 31import java.io.RandomAccessFile; 32import java.security.GeneralSecurityException; 33import java.security.PublicKey; 34import java.security.Signature; 35import java.security.SignatureException; 36import java.security.cert.CertificateFactory; 37import java.security.cert.X509Certificate; 38import java.util.Enumeration; 39import java.util.HashSet; 40import java.util.Iterator; 41import java.util.List; 42import java.util.Locale; 43import java.util.zip.ZipEntry; 44import java.util.zip.ZipFile; 45 46import org.apache.harmony.security.asn1.BerInputStream; 47import org.apache.harmony.security.pkcs7.ContentInfo; 48import org.apache.harmony.security.pkcs7.SignedData; 49import org.apache.harmony.security.pkcs7.SignerInfo; 50import org.apache.harmony.security.x509.Certificate; 51 52/** 53 * RecoverySystem contains methods for interacting with the Android 54 * recovery system (the separate partition that can be used to install 55 * system updates, wipe user data, etc.) 56 */ 57public class RecoverySystem { 58 private static final String TAG = "RecoverySystem"; 59 60 /** 61 * Default location of zip file containing public keys (X509 62 * certs) authorized to sign OTA updates. 63 */ 64 private static final File DEFAULT_KEYSTORE = 65 new File("/system/etc/security/otacerts.zip"); 66 67 /** Send progress to listeners no more often than this (in ms). */ 68 private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; 69 70 /** Used to communicate with recovery. See bootable/recovery/recovery.c. */ 71 private static File RECOVERY_DIR = new File("/cache/recovery"); 72 private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); 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 long fileLen = packageFile.length(); 152 153 RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); 154 try { 155 int lastPercent = 0; 156 long lastPublishTime = System.currentTimeMillis(); 157 if (listener != null) { 158 listener.onProgress(lastPercent); 159 } 160 161 raf.seek(fileLen - 6); 162 byte[] footer = new byte[6]; 163 raf.readFully(footer); 164 165 if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { 166 throw new SignatureException("no signature in file (no footer)"); 167 } 168 169 int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); 170 int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); 171 172 byte[] eocd = new byte[commentSize + 22]; 173 raf.seek(fileLen - (commentSize + 22)); 174 raf.readFully(eocd); 175 176 // Check that we have found the start of the 177 // end-of-central-directory record. 178 if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || 179 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { 180 throw new SignatureException("no signature in file (bad footer)"); 181 } 182 183 for (int i = 4; i < eocd.length-3; ++i) { 184 if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && 185 eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { 186 throw new SignatureException("EOCD marker found after start of EOCD"); 187 } 188 } 189 190 // The following code is largely copied from 191 // JarUtils.verifySignature(). We could just *call* that 192 // method here if that function didn't read the entire 193 // input (ie, the whole OTA package) into memory just to 194 // compute its message digest. 195 196 BerInputStream bis = new BerInputStream( 197 new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); 198 ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis); 199 SignedData signedData = info.getSignedData(); 200 if (signedData == null) { 201 throw new IOException("signedData is null"); 202 } 203 List<Certificate> encCerts = signedData.getCertificates(); 204 if (encCerts.isEmpty()) { 205 throw new IOException("encCerts is empty"); 206 } 207 // Take the first certificate from the signature (packages 208 // should contain only one). 209 Iterator<Certificate> it = encCerts.iterator(); 210 X509Certificate cert = null; 211 if (it.hasNext()) { 212 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 213 InputStream is = new ByteArrayInputStream(it.next().getEncoded()); 214 cert = (X509Certificate) cf.generateCertificate(is); 215 } else { 216 throw new SignatureException("signature contains no certificates"); 217 } 218 219 List<SignerInfo> sigInfos = signedData.getSignerInfos(); 220 SignerInfo sigInfo; 221 if (!sigInfos.isEmpty()) { 222 sigInfo = (SignerInfo)sigInfos.get(0); 223 } else { 224 throw new IOException("no signer infos!"); 225 } 226 227 // Check that the public key of the certificate contained 228 // in the package equals one of our trusted public keys. 229 230 HashSet<X509Certificate> trusted = getTrustedCerts( 231 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); 232 233 PublicKey signatureKey = cert.getPublicKey(); 234 boolean verified = false; 235 for (X509Certificate c : trusted) { 236 if (c.getPublicKey().equals(signatureKey)) { 237 verified = true; 238 break; 239 } 240 } 241 if (!verified) { 242 throw new SignatureException("signature doesn't match any trusted key"); 243 } 244 245 // The signature cert matches a trusted key. Now verify that 246 // the digest in the cert matches the actual file data. 247 248 // The verifier in recovery only handles SHA1withRSA and 249 // SHA256withRSA signatures. SignApk chooses which to use 250 // based on the signature algorithm of the cert: 251 // 252 // "SHA256withRSA" cert -> "SHA256withRSA" signature 253 // "SHA1withRSA" cert -> "SHA1withRSA" signature 254 // "MD5withRSA" cert -> "SHA1withRSA" signature (for backwards compatibility) 255 // any other cert -> SignApk fails 256 // 257 // Here we ignore whatever the cert says, and instead use 258 // whatever algorithm is used by the signature. 259 260 String da = sigInfo.getDigestAlgorithm(); 261 String dea = sigInfo.getDigestEncryptionAlgorithm(); 262 String alg = null; 263 if (da == null || dea == null) { 264 // fall back to the cert algorithm if the sig one 265 // doesn't look right. 266 alg = cert.getSigAlgName(); 267 } else { 268 alg = da + "with" + dea; 269 } 270 Signature sig = Signature.getInstance(alg); 271 sig.initVerify(cert); 272 273 // The signature covers all of the OTA package except the 274 // archive comment and its 2-byte length. 275 long toRead = fileLen - commentSize - 2; 276 long soFar = 0; 277 raf.seek(0); 278 byte[] buffer = new byte[4096]; 279 boolean interrupted = false; 280 while (soFar < toRead) { 281 interrupted = Thread.interrupted(); 282 if (interrupted) break; 283 int size = buffer.length; 284 if (soFar + size > toRead) { 285 size = (int)(toRead - soFar); 286 } 287 int read = raf.read(buffer, 0, size); 288 sig.update(buffer, 0, read); 289 soFar += read; 290 291 if (listener != null) { 292 long now = System.currentTimeMillis(); 293 int p = (int)(soFar * 100 / toRead); 294 if (p > lastPercent && 295 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 296 lastPercent = p; 297 lastPublishTime = now; 298 listener.onProgress(lastPercent); 299 } 300 } 301 } 302 if (listener != null) { 303 listener.onProgress(100); 304 } 305 306 if (interrupted) { 307 throw new SignatureException("verification was interrupted"); 308 } 309 310 if (!sig.verify(sigInfo.getEncryptedDigest())) { 311 throw new SignatureException("signature digest verification failed"); 312 } 313 } finally { 314 raf.close(); 315 } 316 } 317 318 /** 319 * Reboots the device in order to install the given update 320 * package. 321 * Requires the {@link android.Manifest.permission#REBOOT} permission. 322 * 323 * @param context the Context to use 324 * @param packageFile the update package to install. Must be on 325 * a partition mountable by recovery. (The set of partitions 326 * known to recovery may vary from device to device. Generally, 327 * /cache and /data are safe.) 328 * 329 * @throws IOException if writing the recovery command file 330 * fails, or if the reboot itself fails. 331 */ 332 public static void installPackage(Context context, File packageFile) 333 throws IOException { 334 String filename = packageFile.getCanonicalPath(); 335 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); 336 String arg = "--update_package=" + filename + 337 "\n--locale=" + Locale.getDefault().toString(); 338 bootCommand(context, arg); 339 } 340 341 /** 342 * Reboots the device and wipes the user data and cache 343 * partitions. This is sometimes called a "factory reset", which 344 * is something of a misnomer because the system partition is not 345 * restored to its factory state. Requires the 346 * {@link android.Manifest.permission#REBOOT} permission. 347 * 348 * @param context the Context to use 349 * 350 * @throws IOException if writing the recovery command file 351 * fails, or if the reboot itself fails. 352 * @throws SecurityException if the current user is not allowed to wipe data. 353 */ 354 public static void rebootWipeUserData(Context context) throws IOException { 355 rebootWipeUserData(context, false); 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) 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.OWNER, 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 = ""; 399 if (shutdown) { 400 shutdownArg = "--shutdown_after\n"; 401 } 402 403 bootCommand(context, shutdownArg + "--wipe_data\n--locale=" + 404 Locale.getDefault().toString()); 405 } 406 407 /** 408 * Reboot into the recovery system to wipe the /cache partition. 409 * @throws IOException if something goes wrong. 410 */ 411 public static void rebootWipeCache(Context context) throws IOException { 412 bootCommand(context, "--wipe_cache\n--locale=" + Locale.getDefault().toString()); 413 } 414 415 /** 416 * Reboot into the recovery system with the supplied argument. 417 * @param arg to pass to the recovery utility. 418 * @throws IOException if something goes wrong. 419 */ 420 private static void bootCommand(Context context, String arg) throws IOException { 421 RECOVERY_DIR.mkdirs(); // In case we need it 422 COMMAND_FILE.delete(); // In case it's not writable 423 LOG_FILE.delete(); 424 425 FileWriter command = new FileWriter(COMMAND_FILE); 426 try { 427 command.write(arg); 428 command.write("\n"); 429 } finally { 430 command.close(); 431 } 432 433 // Having written the command file, go ahead and reboot 434 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 435 pm.reboot("recovery"); 436 437 throw new IOException("Reboot failed (no permissions?)"); 438 } 439 440 /** 441 * Called after booting to process and remove recovery-related files. 442 * @return the log file from recovery, or null if none was found. 443 * 444 * @hide 445 */ 446 public static String handleAftermath() { 447 // Record the tail of the LOG_FILE 448 String log = null; 449 try { 450 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); 451 } catch (FileNotFoundException e) { 452 Log.i(TAG, "No recovery log file"); 453 } catch (IOException e) { 454 Log.e(TAG, "Error reading recovery log", e); 455 } 456 457 // Delete everything in RECOVERY_DIR except those beginning 458 // with LAST_PREFIX 459 String[] names = RECOVERY_DIR.list(); 460 for (int i = 0; names != null && i < names.length; i++) { 461 if (names[i].startsWith(LAST_PREFIX)) continue; 462 File f = new File(RECOVERY_DIR, names[i]); 463 if (!f.delete()) { 464 Log.e(TAG, "Can't delete: " + f); 465 } else { 466 Log.i(TAG, "Deleted: " + f); 467 } 468 } 469 470 return log; 471 } 472 473 private void RecoverySystem() { } // Do not instantiate 474} 475