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