RecoverySystem.java revision cb95657326add53f81cd2f8a0ae0a1a0527ae799
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. Interrupting 121 * the thread while this function is in progress will result in a 122 * SecurityException being thrown (and the thread's interrupt flag 123 * will be cleared). 124 * 125 * @param packageFile the package to be verified 126 * @param listener an object to receive periodic progress 127 * updates as verification proceeds. May be null. 128 * @param deviceCertsZipFile the zip file of certificates whose 129 * public keys we will accept. Verification succeeds if the 130 * package is signed by the private key corresponding to any 131 * public key in this file. May be null to use the system default 132 * file (currently "/system/etc/security/otacerts.zip"). 133 * 134 * @throws IOException if there were any errors reading the 135 * package or certs files. 136 * @throws GeneralSecurityException if verification failed 137 */ 138 public static void verifyPackage(File packageFile, 139 ProgressListener listener, 140 File deviceCertsZipFile) 141 throws IOException, GeneralSecurityException { 142 long fileLen = packageFile.length(); 143 144 RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); 145 try { 146 int lastPercent = 0; 147 long lastPublishTime = System.currentTimeMillis(); 148 if (listener != null) { 149 listener.onProgress(lastPercent); 150 } 151 152 raf.seek(fileLen - 6); 153 byte[] footer = new byte[6]; 154 raf.readFully(footer); 155 156 if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { 157 throw new SignatureException("no signature in file (no footer)"); 158 } 159 160 int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); 161 int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); 162 Log.v(TAG, String.format("comment size %d; signature start %d", 163 commentSize, signatureStart)); 164 165 byte[] eocd = new byte[commentSize + 22]; 166 raf.seek(fileLen - (commentSize + 22)); 167 raf.readFully(eocd); 168 169 // Check that we have found the start of the 170 // end-of-central-directory record. 171 if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || 172 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { 173 throw new SignatureException("no signature in file (bad footer)"); 174 } 175 176 for (int i = 4; i < eocd.length-3; ++i) { 177 if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && 178 eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { 179 throw new SignatureException("EOCD marker found after start of EOCD"); 180 } 181 } 182 183 // The following code is largely copied from 184 // JarUtils.verifySignature(). We could just *call* that 185 // method here if that function didn't read the entire 186 // input (ie, the whole OTA package) into memory just to 187 // compute its message digest. 188 189 BerInputStream bis = new BerInputStream( 190 new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); 191 ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis); 192 SignedData signedData = info.getSignedData(); 193 if (signedData == null) { 194 throw new IOException("signedData is null"); 195 } 196 Collection encCerts = signedData.getCertificates(); 197 if (encCerts.isEmpty()) { 198 throw new IOException("encCerts is empty"); 199 } 200 // Take the first certificate from the signature (packages 201 // should contain only one). 202 Iterator it = encCerts.iterator(); 203 X509Certificate cert = null; 204 if (it.hasNext()) { 205 cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next()); 206 } else { 207 throw new SignatureException("signature contains no certificates"); 208 } 209 210 List sigInfos = signedData.getSignerInfos(); 211 SignerInfo sigInfo; 212 if (!sigInfos.isEmpty()) { 213 sigInfo = (SignerInfo)sigInfos.get(0); 214 } else { 215 throw new IOException("no signer infos!"); 216 } 217 218 // Check that the public key of the certificate contained 219 // in the package equals one of our trusted public keys. 220 221 HashSet<Certificate> trusted = getTrustedCerts( 222 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); 223 224 PublicKey signatureKey = cert.getPublicKey(); 225 boolean verified = false; 226 for (Certificate c : trusted) { 227 if (c.getPublicKey().equals(signatureKey)) { 228 verified = true; 229 break; 230 } 231 } 232 if (!verified) { 233 throw new SignatureException("signature doesn't match any trusted key"); 234 } 235 236 // The signature cert matches a trusted key. Now verify that 237 // the digest in the cert matches the actual file data. 238 239 // The verifier in recovery *only* handles SHA1withRSA 240 // signatures. SignApk.java always uses SHA1withRSA, no 241 // matter what the cert says to use. Ignore 242 // cert.getSigAlgName(), and instead use whatever 243 // algorithm is used by the signature (which should be 244 // SHA1withRSA). 245 246 String da = sigInfo.getdigestAlgorithm(); 247 String dea = sigInfo.getDigestEncryptionAlgorithm(); 248 String alg = null; 249 if (da == null || dea == null) { 250 // fall back to the cert algorithm if the sig one 251 // doesn't look right. 252 alg = cert.getSigAlgName(); 253 } else { 254 alg = da + "with" + dea; 255 } 256 Signature sig = Signature.getInstance(alg); 257 sig.initVerify(cert); 258 259 // The signature covers all of the OTA package except the 260 // archive comment and its 2-byte length. 261 long toRead = fileLen - commentSize - 2; 262 long soFar = 0; 263 raf.seek(0); 264 byte[] buffer = new byte[4096]; 265 boolean interrupted = false; 266 while (soFar < toRead) { 267 interrupted = Thread.interrupted(); 268 if (interrupted) break; 269 int size = buffer.length; 270 if (soFar + size > toRead) { 271 size = (int)(toRead - soFar); 272 } 273 int read = raf.read(buffer, 0, size); 274 sig.update(buffer, 0, read); 275 soFar += read; 276 277 if (listener != null) { 278 long now = System.currentTimeMillis(); 279 int p = (int)(soFar * 100 / toRead); 280 if (p > lastPercent && 281 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 282 lastPercent = p; 283 lastPublishTime = now; 284 listener.onProgress(lastPercent); 285 } 286 } 287 } 288 if (listener != null) { 289 listener.onProgress(100); 290 } 291 292 if (interrupted) { 293 throw new SignatureException("verification was interrupted"); 294 } 295 296 if (!sig.verify(sigInfo.getEncryptedDigest())) { 297 throw new SignatureException("signature digest verification failed"); 298 } 299 } finally { 300 raf.close(); 301 } 302 } 303 304 /** 305 * Reboots the device in order to install the given update 306 * package. 307 * Requires the {@link android.Manifest.permission#REBOOT} 308 * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} 309 * permissions. 310 * 311 * @param context the Context to use 312 * @param packageFile the update package to install. Currently 313 * must be on the /cache or /data partitions. 314 * 315 * @throws IOException if writing the recovery command file 316 * fails, or if the reboot itself fails. 317 */ 318 public static void installPackage(Context context, File packageFile) 319 throws IOException { 320 String filename = packageFile.getCanonicalPath(); 321 322 if (filename.startsWith("/cache/")) { 323 filename = "CACHE:" + filename.substring(7); 324 } else if (filename.startsWith("/data/")) { 325 filename = "DATA:" + filename.substring(6); 326 } else { 327 throw new IllegalArgumentException( 328 "Must start with /cache or /data: " + filename); 329 } 330 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); 331 String arg = "--update_package=" + filename; 332 bootCommand(context, arg); 333 } 334 335 /** 336 * Reboots the device and wipes the user data partition. This is 337 * sometimes called a "factory reset", which is something of a 338 * misnomer because the system partition is not restored to its 339 * factory state. 340 * Requires the {@link android.Manifest.permission#REBOOT} 341 * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} 342 * permissions. 343 * 344 * @param context the Context to use 345 * 346 * @throws IOException if writing the recovery command file 347 * fails, or if the reboot itself fails. 348 */ 349 public static void rebootWipeUserData(Context context) 350 throws IOException { 351 bootCommand(context, "--wipe_data"); 352 } 353 354 /** 355 * Reboot into the recovery system to wipe the /data partition and toggle 356 * Encrypted File Systems on/off. 357 * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. 358 * @throws IOException if something goes wrong. 359 * 360 * @hide 361 */ 362 public static void rebootToggleEFS(Context context, boolean efsEnabled) 363 throws IOException { 364 if (efsEnabled) { 365 bootCommand(context, "--set_encrypted_filesystem=on"); 366 } else { 367 bootCommand(context, "--set_encrypted_filesystem=off"); 368 } 369 } 370 371 /** 372 * Reboot into the recovery system with the supplied argument. 373 * @param arg to pass to the recovery utility. 374 * @throws IOException if something goes wrong. 375 */ 376 private static void bootCommand(Context context, String arg) throws IOException { 377 RECOVERY_DIR.mkdirs(); // In case we need it 378 COMMAND_FILE.delete(); // In case it's not writable 379 LOG_FILE.delete(); 380 381 FileWriter command = new FileWriter(COMMAND_FILE); 382 try { 383 command.write(arg); 384 command.write("\n"); 385 } finally { 386 command.close(); 387 } 388 389 // Having written the command file, go ahead and reboot 390 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 391 pm.reboot("recovery"); 392 393 throw new IOException("Reboot failed (no permissions?)"); 394 } 395 396 /** 397 * Called after booting to process and remove recovery-related files. 398 * @return the log file from recovery, or null if none was found. 399 * 400 * @hide 401 */ 402 public static String handleAftermath() { 403 // Record the tail of the LOG_FILE 404 String log = null; 405 try { 406 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); 407 } catch (FileNotFoundException e) { 408 Log.i(TAG, "No recovery log file"); 409 } catch (IOException e) { 410 Log.e(TAG, "Error reading recovery log", e); 411 } 412 413 // Delete everything in RECOVERY_DIR 414 String[] names = RECOVERY_DIR.list(); 415 for (int i = 0; names != null && i < names.length; i++) { 416 File f = new File(RECOVERY_DIR, names[i]); 417 if (!f.delete()) { 418 Log.e(TAG, "Can't delete: " + f); 419 } else { 420 Log.i(TAG, "Deleted: " + f); 421 } 422 } 423 424 return log; 425 } 426 427 private void RecoverySystem() { } // Do not instantiate 428} 429