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