RecoverySystem.java revision 0cb047c0de7fe6a775bc6e9cff194af9dcb726cf
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 java.io.ByteArrayInputStream; 20import java.io.File; 21import java.io.FileNotFoundException; 22import java.io.FileWriter; 23import java.io.IOException; 24import java.io.RandomAccessFile; 25import java.security.GeneralSecurityException; 26import java.security.PublicKey; 27import java.security.Signature; 28import java.security.SignatureException; 29import java.security.cert.Certificate; 30import java.security.cert.CertificateFactory; 31import java.security.cert.X509Certificate; 32import java.util.Collection; 33import java.util.Enumeration; 34import java.util.HashSet; 35import java.util.Iterator; 36import java.util.List; 37import java.util.zip.ZipEntry; 38import java.util.zip.ZipFile; 39 40import android.content.Context; 41import android.util.Log; 42 43import org.apache.harmony.security.asn1.BerInputStream; 44import org.apache.harmony.security.pkcs7.ContentInfo; 45import org.apache.harmony.security.pkcs7.SignedData; 46import org.apache.harmony.security.pkcs7.SignerInfo; 47import org.apache.harmony.security.provider.cert.X509CertImpl; 48 49/** 50 * RecoverySystem contains methods for interacting with the Android 51 * recovery system (the separate partition that can be used to install 52 * system updates, wipe user data, etc.) 53 */ 54public class RecoverySystem { 55 private static final String TAG = "RecoverySystem"; 56 57 /** 58 * Default location of zip file containing public keys (X509 59 * certs) authorized to sign OTA updates. 60 */ 61 private static final File DEFAULT_KEYSTORE = 62 new File("/system/etc/security/otacerts.zip"); 63 64 /** Send progress to listeners no more often than this (in ms). */ 65 private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; 66 67 /** Used to communicate with recovery. See bootable/recovery/recovery.c. */ 68 private static File RECOVERY_DIR = new File("/cache/recovery"); 69 private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); 70 private static File LOG_FILE = new File(RECOVERY_DIR, "log"); 71 72 // Length limits for reading files. 73 private static int LOG_FILE_MAX_LENGTH = 8 * 1024; 74 75 /** 76 * Interface definition for a callback to be invoked regularly as 77 * verification proceeds. 78 */ 79 public interface ProgressListener { 80 /** 81 * Called periodically as the verification progresses. 82 * 83 * @param progress the approximate percentage of the 84 * verification that has been completed, ranging from 0 85 * to 100 (inclusive). 86 */ 87 public void onProgress(int progress); 88 } 89 90 /** @return the set of certs that can be used to sign an OTA package. */ 91 private static HashSet<Certificate> getTrustedCerts(File keystore) 92 throws IOException, GeneralSecurityException { 93 HashSet<Certificate> trusted = new HashSet<Certificate>(); 94 if (keystore == null) { 95 keystore = DEFAULT_KEYSTORE; 96 } 97 ZipFile zip = new ZipFile(keystore); 98 try { 99 CertificateFactory cf = CertificateFactory.getInstance("X.509"); 100 Enumeration<? extends ZipEntry> entries = zip.entries(); 101 while (entries.hasMoreElements()) { 102 ZipEntry entry = entries.nextElement(); 103 trusted.add(cf.generateCertificate(zip.getInputStream(entry))); 104 } 105 } finally { 106 zip.close(); 107 } 108 return trusted; 109 } 110 111 /** 112 * Verify the cryptographic signature of a system update package 113 * before installing it. Note that the package is also verified 114 * separately by the installer once the device is rebooted into 115 * the recovery system. This function will return only if the 116 * package was successfully verified; otherwise it will throw an 117 * exception. 118 * 119 * Verification of a package can take significant time, so this 120 * function should not be called from a UI thread. 121 * 122 * @param packageFile the package to be verified 123 * @param listener an object to receive periodic progress 124 * updates as verification proceeds. May be null. 125 * @param deviceCertsZipFile the zip file of certificates whose 126 * public keys we will accept. Verification succeeds if the 127 * package is signed by the private key corresponding to any 128 * public key in this file. May be null to use the system default 129 * file (currently "/system/etc/security/otacerts.zip"). 130 * 131 * @throws IOException if there were any errors reading the 132 * package or certs files. 133 * @throws GeneralSecurityException if verification failed 134 */ 135 public static void verifyPackage(File packageFile, 136 ProgressListener listener, 137 File deviceCertsZipFile) 138 throws IOException, GeneralSecurityException { 139 long fileLen = packageFile.length(); 140 141 RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); 142 try { 143 int lastPercent = 0; 144 long lastPublishTime = System.currentTimeMillis(); 145 if (listener != null) { 146 listener.onProgress(lastPercent); 147 } 148 149 raf.seek(fileLen - 6); 150 byte[] footer = new byte[6]; 151 raf.readFully(footer); 152 153 if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { 154 throw new SignatureException("no signature in file (no footer)"); 155 } 156 157 int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); 158 int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); 159 Log.v(TAG, String.format("comment size %d; signature start %d", 160 commentSize, signatureStart)); 161 162 byte[] eocd = new byte[commentSize + 22]; 163 raf.seek(fileLen - (commentSize + 22)); 164 raf.readFully(eocd); 165 166 // Check that we have found the start of the 167 // end-of-central-directory record. 168 if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || 169 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { 170 throw new SignatureException("no signature in file (bad footer)"); 171 } 172 173 for (int i = 4; i < eocd.length-3; ++i) { 174 if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && 175 eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { 176 throw new SignatureException("EOCD marker found after start of EOCD"); 177 } 178 } 179 180 // The following code is largely copied from 181 // JarUtils.verifySignature(). We could just *call* that 182 // method here if that function didn't read the entire 183 // input (ie, the whole OTA package) into memory just to 184 // compute its message digest. 185 186 BerInputStream bis = new BerInputStream( 187 new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); 188 ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis); 189 SignedData signedData = info.getSignedData(); 190 if (signedData == null) { 191 throw new IOException("signedData is null"); 192 } 193 Collection encCerts = signedData.getCertificates(); 194 if (encCerts.isEmpty()) { 195 throw new IOException("encCerts is empty"); 196 } 197 // Take the first certificate from the signature (packages 198 // should contain only one). 199 Iterator it = encCerts.iterator(); 200 X509Certificate cert = null; 201 if (it.hasNext()) { 202 cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next()); 203 } else { 204 throw new SignatureException("signature contains no certificates"); 205 } 206 207 List sigInfos = signedData.getSignerInfos(); 208 SignerInfo sigInfo; 209 if (!sigInfos.isEmpty()) { 210 sigInfo = (SignerInfo)sigInfos.get(0); 211 } else { 212 throw new IOException("no signer infos!"); 213 } 214 215 // Check that the public key of the certificate contained 216 // in the package equals one of our trusted public keys. 217 218 HashSet<Certificate> trusted = getTrustedCerts( 219 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); 220 221 PublicKey signatureKey = cert.getPublicKey(); 222 boolean verified = false; 223 for (Certificate c : trusted) { 224 if (c.getPublicKey().equals(signatureKey)) { 225 verified = true; 226 break; 227 } 228 } 229 if (!verified) { 230 throw new SignatureException("signature doesn't match any trusted key"); 231 } 232 233 // The signature cert matches a trusted key. Now verify that 234 // the digest in the cert matches the actual file data. 235 236 // The verifier in recovery *only* handles SHA1withRSA 237 // signatures. SignApk.java always uses SHA1withRSA, no 238 // matter what the cert says to use. Ignore 239 // cert.getSigAlgName(), and instead use whatever 240 // algorithm is used by the signature (which should be 241 // SHA1withRSA). 242 243 String da = sigInfo.getdigestAlgorithm(); 244 String dea = sigInfo.getDigestEncryptionAlgorithm(); 245 String alg = null; 246 if (da == null || dea == null) { 247 // fall back to the cert algorithm if the sig one 248 // doesn't look right. 249 alg = cert.getSigAlgName(); 250 } else { 251 alg = da + "with" + dea; 252 } 253 Signature sig = Signature.getInstance(alg); 254 sig.initVerify(cert); 255 256 // The signature covers all of the OTA package except the 257 // archive comment and its 2-byte length. 258 long toRead = fileLen - commentSize - 2; 259 long soFar = 0; 260 raf.seek(0); 261 byte[] buffer = new byte[4096]; 262 while (soFar < toRead) { 263 int size = buffer.length; 264 if (soFar + size > toRead) { 265 size = (int)(toRead - soFar); 266 } 267 int read = raf.read(buffer, 0, size); 268 sig.update(buffer, 0, read); 269 soFar += read; 270 271 if (listener != null) { 272 long now = System.currentTimeMillis(); 273 int p = (int)(soFar * 100 / toRead); 274 if (p > lastPercent && 275 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 276 lastPercent = p; 277 lastPublishTime = now; 278 listener.onProgress(lastPercent); 279 } 280 } 281 } 282 if (listener != null) { 283 listener.onProgress(100); 284 } 285 286 if (!sig.verify(sigInfo.getEncryptedDigest())) { 287 throw new SignatureException("signature digest verification failed"); 288 } 289 } finally { 290 raf.close(); 291 } 292 } 293 294 /** 295 * Reboots the device in order to install the given update 296 * package. 297 * Requires the {@link android.Manifest.permission#REBOOT} 298 * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} 299 * permissions. 300 * 301 * @param context the Context to use 302 * @param packageFile the update package to install. Currently 303 * must be on the /cache or /data partitions. 304 * 305 * @throws IOException if writing the recovery command file 306 * fails, or if the reboot itself fails. 307 */ 308 public static void installPackage(Context context, File packageFile) 309 throws IOException { 310 String filename = packageFile.getCanonicalPath(); 311 312 if (filename.startsWith("/cache/")) { 313 filename = "CACHE:" + filename.substring(7); 314 } else if (filename.startsWith("/data/")) { 315 filename = "DATA:" + filename.substring(6); 316 } else { 317 throw new IllegalArgumentException( 318 "Must start with /cache or /data: " + filename); 319 } 320 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); 321 String arg = "--update_package=" + filename; 322 bootCommand(context, arg); 323 } 324 325 /** 326 * Reboots the device and wipes the user data partition. This is 327 * sometimes called a "factory reset", which is something of a 328 * misnomer because the system partition is not restored to its 329 * factory state. 330 * Requires the {@link android.Manifest.permission#REBOOT} 331 * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} 332 * permissions. 333 * 334 * @param context the Context to use 335 * 336 * @throws IOException if writing the recovery command file 337 * fails, or if the reboot itself fails. 338 */ 339 public static void rebootWipeUserData(Context context) 340 throws IOException { 341 bootCommand(context, "--wipe_data"); 342 } 343 344 /** 345 * Reboot into the recovery system to wipe the /data partition and toggle 346 * Encrypted File Systems on/off. 347 * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. 348 * @throws IOException if something goes wrong. 349 * 350 * @hide 351 */ 352 public static void rebootToggleEFS(Context context, boolean efsEnabled) 353 throws IOException { 354 if (efsEnabled) { 355 bootCommand(context, "--set_encrypted_filesystem=on"); 356 } else { 357 bootCommand(context, "--set_encrypted_filesystem=off"); 358 } 359 } 360 361 /** 362 * Reboot into the recovery system with the supplied argument. 363 * @param arg to pass to the recovery utility. 364 * @throws IOException if something goes wrong. 365 */ 366 private static void bootCommand(Context context, String arg) throws IOException { 367 RECOVERY_DIR.mkdirs(); // In case we need it 368 COMMAND_FILE.delete(); // In case it's not writable 369 LOG_FILE.delete(); 370 371 FileWriter command = new FileWriter(COMMAND_FILE); 372 try { 373 command.write(arg); 374 command.write("\n"); 375 } finally { 376 command.close(); 377 } 378 379 // Having written the command file, go ahead and reboot 380 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 381 pm.reboot("recovery"); 382 383 throw new IOException("Reboot failed (no permissions?)"); 384 } 385 386 /** 387 * Called after booting to process and remove recovery-related files. 388 * @return the log file from recovery, or null if none was found. 389 * 390 * @hide 391 */ 392 public static String handleAftermath() { 393 // Record the tail of the LOG_FILE 394 String log = null; 395 try { 396 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); 397 } catch (FileNotFoundException e) { 398 Log.i(TAG, "No recovery log file"); 399 } catch (IOException e) { 400 Log.e(TAG, "Error reading recovery log", e); 401 } 402 403 // Delete everything in RECOVERY_DIR 404 String[] names = RECOVERY_DIR.list(); 405 for (int i = 0; names != null && i < names.length; i++) { 406 File f = new File(RECOVERY_DIR, names[i]); 407 if (!f.delete()) { 408 Log.e(TAG, "Can't delete: " + f); 409 } else { 410 Log.i(TAG, "Deleted: " + f); 411 } 412 } 413 414 return log; 415 } 416 417 private void RecoverySystem() { } // Do not instantiate 418} 419