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 
17 package android.os;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.os.UserManager;
23 import android.text.TextUtils;
24 import android.util.Log;
25 
26 import java.io.ByteArrayInputStream;
27 import java.io.File;
28 import java.io.FileNotFoundException;
29 import java.io.FileWriter;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.RandomAccessFile;
33 import java.security.GeneralSecurityException;
34 import java.security.PublicKey;
35 import java.security.Signature;
36 import java.security.SignatureException;
37 import java.security.cert.CertificateFactory;
38 import java.security.cert.X509Certificate;
39 import java.util.Enumeration;
40 import java.util.HashSet;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.zip.ZipEntry;
45 import java.util.zip.ZipFile;
46 
47 import org.apache.harmony.security.asn1.BerInputStream;
48 import org.apache.harmony.security.pkcs7.ContentInfo;
49 import org.apache.harmony.security.pkcs7.SignedData;
50 import org.apache.harmony.security.pkcs7.SignerInfo;
51 import org.apache.harmony.security.x509.Certificate;
52 
53 /**
54  * RecoverySystem contains methods for interacting with the Android
55  * recovery system (the separate partition that can be used to install
56  * system updates, wipe user data, etc.)
57  */
58 public class RecoverySystem {
59     private static final String TAG = "RecoverySystem";
60 
61     /**
62      * Default location of zip file containing public keys (X509
63      * certs) authorized to sign OTA updates.
64      */
65     private static final File DEFAULT_KEYSTORE =
66         new File("/system/etc/security/otacerts.zip");
67 
68     /** Send progress to listeners no more often than this (in ms). */
69     private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
70 
71     /** Used to communicate with recovery.  See bootable/recovery/recovery.c. */
72     private static File RECOVERY_DIR = new File("/cache/recovery");
73     private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
74     private static File LOG_FILE = new File(RECOVERY_DIR, "log");
75     private static String LAST_PREFIX = "last_";
76 
77     // Length limits for reading files.
78     private static int LOG_FILE_MAX_LENGTH = 64 * 1024;
79 
80     /**
81      * Interface definition for a callback to be invoked regularly as
82      * verification proceeds.
83      */
84     public interface ProgressListener {
85         /**
86          * Called periodically as the verification progresses.
87          *
88          * @param progress  the approximate percentage of the
89          *        verification that has been completed, ranging from 0
90          *        to 100 (inclusive).
91          */
onProgress(int progress)92         public void onProgress(int progress);
93     }
94 
95     /** @return the set of certs that can be used to sign an OTA package. */
getTrustedCerts(File keystore)96     private static HashSet<X509Certificate> getTrustedCerts(File keystore)
97         throws IOException, GeneralSecurityException {
98         HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
99         if (keystore == null) {
100             keystore = DEFAULT_KEYSTORE;
101         }
102         ZipFile zip = new ZipFile(keystore);
103         try {
104             CertificateFactory cf = CertificateFactory.getInstance("X.509");
105             Enumeration<? extends ZipEntry> entries = zip.entries();
106             while (entries.hasMoreElements()) {
107                 ZipEntry entry = entries.nextElement();
108                 InputStream is = zip.getInputStream(entry);
109                 try {
110                     trusted.add((X509Certificate) cf.generateCertificate(is));
111                 } finally {
112                     is.close();
113                 }
114             }
115         } finally {
116             zip.close();
117         }
118         return trusted;
119     }
120 
121     /**
122      * Verify the cryptographic signature of a system update package
123      * before installing it.  Note that the package is also verified
124      * separately by the installer once the device is rebooted into
125      * the recovery system.  This function will return only if the
126      * package was successfully verified; otherwise it will throw an
127      * exception.
128      *
129      * Verification of a package can take significant time, so this
130      * function should not be called from a UI thread.  Interrupting
131      * the thread while this function is in progress will result in a
132      * SecurityException being thrown (and the thread's interrupt flag
133      * will be cleared).
134      *
135      * @param packageFile  the package to be verified
136      * @param listener     an object to receive periodic progress
137      * updates as verification proceeds.  May be null.
138      * @param deviceCertsZipFile  the zip file of certificates whose
139      * public keys we will accept.  Verification succeeds if the
140      * package is signed by the private key corresponding to any
141      * public key in this file.  May be null to use the system default
142      * file (currently "/system/etc/security/otacerts.zip").
143      *
144      * @throws IOException if there were any errors reading the
145      * package or certs files.
146      * @throws GeneralSecurityException if verification failed
147      */
verifyPackage(File packageFile, ProgressListener listener, File deviceCertsZipFile)148     public static void verifyPackage(File packageFile,
149                                      ProgressListener listener,
150                                      File deviceCertsZipFile)
151         throws IOException, GeneralSecurityException {
152         long fileLen = packageFile.length();
153 
154         RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
155         try {
156             int lastPercent = 0;
157             long lastPublishTime = System.currentTimeMillis();
158             if (listener != null) {
159                 listener.onProgress(lastPercent);
160             }
161 
162             raf.seek(fileLen - 6);
163             byte[] footer = new byte[6];
164             raf.readFully(footer);
165 
166             if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
167                 throw new SignatureException("no signature in file (no footer)");
168             }
169 
170             int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
171             int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
172 
173             byte[] eocd = new byte[commentSize + 22];
174             raf.seek(fileLen - (commentSize + 22));
175             raf.readFully(eocd);
176 
177             // Check that we have found the start of the
178             // end-of-central-directory record.
179             if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
180                 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
181                 throw new SignatureException("no signature in file (bad footer)");
182             }
183 
184             for (int i = 4; i < eocd.length-3; ++i) {
185                 if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
186                     eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
187                     throw new SignatureException("EOCD marker found after start of EOCD");
188                 }
189             }
190 
191             // The following code is largely copied from
192             // JarUtils.verifySignature().  We could just *call* that
193             // method here if that function didn't read the entire
194             // input (ie, the whole OTA package) into memory just to
195             // compute its message digest.
196 
197             BerInputStream bis = new BerInputStream(
198                 new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
199             ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
200             SignedData signedData = info.getSignedData();
201             if (signedData == null) {
202                 throw new IOException("signedData is null");
203             }
204             List<Certificate> encCerts = signedData.getCertificates();
205             if (encCerts.isEmpty()) {
206                 throw new IOException("encCerts is empty");
207             }
208             // Take the first certificate from the signature (packages
209             // should contain only one).
210             Iterator<Certificate> it = encCerts.iterator();
211             X509Certificate cert = null;
212             if (it.hasNext()) {
213                 CertificateFactory cf = CertificateFactory.getInstance("X.509");
214                 InputStream is = new ByteArrayInputStream(it.next().getEncoded());
215                 cert = (X509Certificate) cf.generateCertificate(is);
216             } else {
217                 throw new SignatureException("signature contains no certificates");
218             }
219 
220             List<SignerInfo> sigInfos = signedData.getSignerInfos();
221             SignerInfo sigInfo;
222             if (!sigInfos.isEmpty()) {
223                 sigInfo = (SignerInfo)sigInfos.get(0);
224             } else {
225                 throw new IOException("no signer infos!");
226             }
227 
228             // Check that the public key of the certificate contained
229             // in the package equals one of our trusted public keys.
230 
231             HashSet<X509Certificate> trusted = getTrustedCerts(
232                 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
233 
234             PublicKey signatureKey = cert.getPublicKey();
235             boolean verified = false;
236             for (X509Certificate c : trusted) {
237                 if (c.getPublicKey().equals(signatureKey)) {
238                     verified = true;
239                     break;
240                 }
241             }
242             if (!verified) {
243                 throw new SignatureException("signature doesn't match any trusted key");
244             }
245 
246             // The signature cert matches a trusted key.  Now verify that
247             // the digest in the cert matches the actual file data.
248 
249             // The verifier in recovery only handles SHA1withRSA and
250             // SHA256withRSA signatures.  SignApk chooses which to use
251             // based on the signature algorithm of the cert:
252             //
253             //    "SHA256withRSA" cert -> "SHA256withRSA" signature
254             //    "SHA1withRSA" cert   -> "SHA1withRSA" signature
255             //    "MD5withRSA" cert    -> "SHA1withRSA" signature (for backwards compatibility)
256             //    any other cert       -> SignApk fails
257             //
258             // Here we ignore whatever the cert says, and instead use
259             // whatever algorithm is used by the signature.
260 
261             String da = sigInfo.getDigestAlgorithm();
262             String dea = sigInfo.getDigestEncryptionAlgorithm();
263             String alg = null;
264             if (da == null || dea == null) {
265                 // fall back to the cert algorithm if the sig one
266                 // doesn't look right.
267                 alg = cert.getSigAlgName();
268             } else {
269                 alg = da + "with" + dea;
270             }
271             Signature sig = Signature.getInstance(alg);
272             sig.initVerify(cert);
273 
274             // The signature covers all of the OTA package except the
275             // archive comment and its 2-byte length.
276             long toRead = fileLen - commentSize - 2;
277             long soFar = 0;
278             raf.seek(0);
279             byte[] buffer = new byte[4096];
280             boolean interrupted = false;
281             while (soFar < toRead) {
282                 interrupted = Thread.interrupted();
283                 if (interrupted) break;
284                 int size = buffer.length;
285                 if (soFar + size > toRead) {
286                     size = (int)(toRead - soFar);
287                 }
288                 int read = raf.read(buffer, 0, size);
289                 sig.update(buffer, 0, read);
290                 soFar += read;
291 
292                 if (listener != null) {
293                     long now = System.currentTimeMillis();
294                     int p = (int)(soFar * 100 / toRead);
295                     if (p > lastPercent &&
296                         now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
297                         lastPercent = p;
298                         lastPublishTime = now;
299                         listener.onProgress(lastPercent);
300                     }
301                 }
302             }
303             if (listener != null) {
304                 listener.onProgress(100);
305             }
306 
307             if (interrupted) {
308                 throw new SignatureException("verification was interrupted");
309             }
310 
311             if (!sig.verify(sigInfo.getEncryptedDigest())) {
312                 throw new SignatureException("signature digest verification failed");
313             }
314         } finally {
315             raf.close();
316         }
317     }
318 
319     /**
320      * Reboots the device in order to install the given update
321      * package.
322      * Requires the {@link android.Manifest.permission#REBOOT} permission.
323      *
324      * @param context      the Context to use
325      * @param packageFile  the update package to install.  Must be on
326      * a partition mountable by recovery.  (The set of partitions
327      * known to recovery may vary from device to device.  Generally,
328      * /cache and /data are safe.)
329      *
330      * @throws IOException  if writing the recovery command file
331      * fails, or if the reboot itself fails.
332      */
installPackage(Context context, File packageFile)333     public static void installPackage(Context context, File packageFile)
334         throws IOException {
335         String filename = packageFile.getCanonicalPath();
336         Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
337 
338         final String filenameArg = "--update_package=" + filename;
339         final String localeArg = "--locale=" + Locale.getDefault().toString();
340         bootCommand(context, filenameArg, localeArg);
341     }
342 
343     /**
344      * Reboots the device and wipes the user data and cache
345      * partitions.  This is sometimes called a "factory reset", which
346      * is something of a misnomer because the system partition is not
347      * restored to its factory state.  Requires the
348      * {@link android.Manifest.permission#REBOOT} permission.
349      *
350      * @param context  the Context to use
351      *
352      * @throws IOException  if writing the recovery command file
353      * fails, or if the reboot itself fails.
354      * @throws SecurityException if the current user is not allowed to wipe data.
355      */
rebootWipeUserData(Context context)356     public static void rebootWipeUserData(Context context) throws IOException {
357         rebootWipeUserData(context, false, context.getPackageName());
358     }
359 
360     /** {@hide} */
rebootWipeUserData(Context context, String reason)361     public static void rebootWipeUserData(Context context, String reason) throws IOException {
362         rebootWipeUserData(context, false, reason);
363     }
364 
365     /** {@hide} */
rebootWipeUserData(Context context, boolean shutdown)366     public static void rebootWipeUserData(Context context, boolean shutdown)
367             throws IOException {
368         rebootWipeUserData(context, shutdown, context.getPackageName());
369     }
370 
371     /**
372      * Reboots the device and wipes the user data and cache
373      * partitions.  This is sometimes called a "factory reset", which
374      * is something of a misnomer because the system partition is not
375      * restored to its factory state.  Requires the
376      * {@link android.Manifest.permission#REBOOT} permission.
377      *
378      * @param context   the Context to use
379      * @param shutdown  if true, the device will be powered down after
380      *                  the wipe completes, rather than being rebooted
381      *                  back to the regular system.
382      *
383      * @throws IOException  if writing the recovery command file
384      * fails, or if the reboot itself fails.
385      * @throws SecurityException if the current user is not allowed to wipe data.
386      *
387      * @hide
388      */
rebootWipeUserData(Context context, boolean shutdown, String reason)389     public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
390             throws IOException {
391         UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
392         if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
393             throw new SecurityException("Wiping data is not allowed for this user.");
394         }
395         final ConditionVariable condition = new ConditionVariable();
396 
397         Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
398         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
399         context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
400                 android.Manifest.permission.MASTER_CLEAR,
401                 new BroadcastReceiver() {
402                     @Override
403                     public void onReceive(Context context, Intent intent) {
404                         condition.open();
405                     }
406                 }, null, 0, null, null);
407 
408         // Block until the ordered broadcast has completed.
409         condition.block();
410 
411         String shutdownArg = null;
412         if (shutdown) {
413             shutdownArg = "--shutdown_after";
414         }
415 
416         String reasonArg = null;
417         if (!TextUtils.isEmpty(reason)) {
418             reasonArg = "--reason=" + sanitizeArg(reason);
419         }
420 
421         final String localeArg = "--locale=" + Locale.getDefault().toString();
422         bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
423     }
424 
425     /**
426      * Reboot into the recovery system to wipe the /cache partition.
427      * @throws IOException if something goes wrong.
428      */
rebootWipeCache(Context context)429     public static void rebootWipeCache(Context context) throws IOException {
430         rebootWipeCache(context, context.getPackageName());
431     }
432 
433     /** {@hide} */
rebootWipeCache(Context context, String reason)434     public static void rebootWipeCache(Context context, String reason) throws IOException {
435         String reasonArg = null;
436         if (!TextUtils.isEmpty(reason)) {
437             reasonArg = "--reason=" + sanitizeArg(reason);
438         }
439 
440         final String localeArg = "--locale=" + Locale.getDefault().toString();
441         bootCommand(context, "--wipe_cache", reasonArg, localeArg);
442     }
443 
444     /**
445      * Reboot into the recovery system with the supplied argument.
446      * @param args to pass to the recovery utility.
447      * @throws IOException if something goes wrong.
448      */
bootCommand(Context context, String... args)449     private static void bootCommand(Context context, String... args) throws IOException {
450         RECOVERY_DIR.mkdirs();  // In case we need it
451         COMMAND_FILE.delete();  // In case it's not writable
452         LOG_FILE.delete();
453 
454         FileWriter command = new FileWriter(COMMAND_FILE);
455         try {
456             for (String arg : args) {
457                 if (!TextUtils.isEmpty(arg)) {
458                     command.write(arg);
459                     command.write("\n");
460                 }
461             }
462         } finally {
463             command.close();
464         }
465 
466         // Having written the command file, go ahead and reboot
467         PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
468         pm.reboot(PowerManager.REBOOT_RECOVERY);
469 
470         throw new IOException("Reboot failed (no permissions?)");
471     }
472 
473     /**
474      * Called after booting to process and remove recovery-related files.
475      * @return the log file from recovery, or null if none was found.
476      *
477      * @hide
478      */
handleAftermath()479     public static String handleAftermath() {
480         // Record the tail of the LOG_FILE
481         String log = null;
482         try {
483             log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
484         } catch (FileNotFoundException e) {
485             Log.i(TAG, "No recovery log file");
486         } catch (IOException e) {
487             Log.e(TAG, "Error reading recovery log", e);
488         }
489 
490         // Delete everything in RECOVERY_DIR except those beginning
491         // with LAST_PREFIX
492         String[] names = RECOVERY_DIR.list();
493         for (int i = 0; names != null && i < names.length; i++) {
494             if (names[i].startsWith(LAST_PREFIX)) continue;
495             File f = new File(RECOVERY_DIR, names[i]);
496             if (!f.delete()) {
497                 Log.e(TAG, "Can't delete: " + f);
498             } else {
499                 Log.i(TAG, "Deleted: " + f);
500             }
501         }
502 
503         return log;
504     }
505 
506     /**
507      * Internally, recovery treats each line of the command file as a separate
508      * argv, so we only need to protect against newlines and nulls.
509      */
sanitizeArg(String arg)510     private static String sanitizeArg(String arg) {
511         arg = arg.replace('\0', '?');
512         arg = arg.replace('\n', '?');
513         return arg;
514     }
515 
RecoverySystem()516     private void RecoverySystem() { }  // Do not instantiate
517 }
518