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