RecoverySystem.java revision 9ad08ec5be0d1e225c9f463fd395ba852b6b5bba
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 sun.security.pkcs.PKCS7;
48import sun.security.pkcs.SignerInfo;
49
50/**
51 * RecoverySystem contains methods for interacting with the Android
52 * recovery system (the separate partition that can be used to install
53 * system updates, wipe user data, etc.)
54 */
55public class RecoverySystem {
56    private static final String TAG = "RecoverySystem";
57
58    /**
59     * Default location of zip file containing public keys (X509
60     * certs) authorized to sign OTA updates.
61     */
62    private static final File DEFAULT_KEYSTORE =
63        new File("/system/etc/security/otacerts.zip");
64
65    /** Send progress to listeners no more often than this (in ms). */
66    private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
67
68    /** Used to communicate with recovery.  See bootable/recovery/recovery.cpp. */
69    private static File RECOVERY_DIR = new File("/cache/recovery");
70    private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
71    private static File UNCRYPT_FILE = new File(RECOVERY_DIR, "uncrypt_file");
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<X509Certificate> getTrustedCerts(File keystore)
95        throws IOException, GeneralSecurityException {
96        HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
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                InputStream is = zip.getInputStream(entry);
107                try {
108                    trusted.add((X509Certificate) cf.generateCertificate(is));
109                } finally {
110                    is.close();
111                }
112            }
113        } finally {
114            zip.close();
115        }
116        return trusted;
117    }
118
119    /**
120     * Verify the cryptographic signature of a system update package
121     * before installing it.  Note that the package is also verified
122     * separately by the installer once the device is rebooted into
123     * the recovery system.  This function will return only if the
124     * package was successfully verified; otherwise it will throw an
125     * exception.
126     *
127     * Verification of a package can take significant time, so this
128     * function should not be called from a UI thread.  Interrupting
129     * the thread while this function is in progress will result in a
130     * SecurityException being thrown (and the thread's interrupt flag
131     * will be cleared).
132     *
133     * @param packageFile  the package to be verified
134     * @param listener     an object to receive periodic progress
135     * updates as verification proceeds.  May be null.
136     * @param deviceCertsZipFile  the zip file of certificates whose
137     * public keys we will accept.  Verification succeeds if the
138     * package is signed by the private key corresponding to any
139     * public key in this file.  May be null to use the system default
140     * file (currently "/system/etc/security/otacerts.zip").
141     *
142     * @throws IOException if there were any errors reading the
143     * package or certs files.
144     * @throws GeneralSecurityException if verification failed
145     */
146    public static void verifyPackage(File packageFile,
147                                     ProgressListener listener,
148                                     File deviceCertsZipFile)
149        throws IOException, GeneralSecurityException {
150        final long fileLen = packageFile.length();
151
152        final RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
153        try {
154            final long startTimeMillis = System.currentTimeMillis();
155            if (listener != null) {
156                listener.onProgress(0);
157            }
158
159            raf.seek(fileLen - 6);
160            byte[] footer = new byte[6];
161            raf.readFully(footer);
162
163            if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
164                throw new SignatureException("no signature in file (no footer)");
165            }
166
167            final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
168            final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
169
170            byte[] eocd = new byte[commentSize + 22];
171            raf.seek(fileLen - (commentSize + 22));
172            raf.readFully(eocd);
173
174            // Check that we have found the start of the
175            // end-of-central-directory record.
176            if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
177                eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
178                throw new SignatureException("no signature in file (bad footer)");
179            }
180
181            for (int i = 4; i < eocd.length-3; ++i) {
182                if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
183                    eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
184                    throw new SignatureException("EOCD marker found after start of EOCD");
185                }
186            }
187
188            // Parse the signature
189            PKCS7 block =
190                new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
191
192            // Take the first certificate from the signature (packages
193            // should contain only one).
194            X509Certificate[] certificates = block.getCertificates();
195            if (certificates == null || certificates.length == 0) {
196                throw new SignatureException("signature contains no certificates");
197            }
198            X509Certificate cert = certificates[0];
199            PublicKey signatureKey = cert.getPublicKey();
200
201            SignerInfo[] signerInfos = block.getSignerInfos();
202            if (signerInfos == null || signerInfos.length == 0) {
203                throw new SignatureException("signature contains no signedData");
204            }
205            SignerInfo signerInfo = signerInfos[0];
206
207            // Check that the public key of the certificate contained
208            // in the package equals one of our trusted public keys.
209            boolean verified = false;
210            HashSet<X509Certificate> trusted = getTrustedCerts(
211                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
212            for (X509Certificate c : trusted) {
213                if (c.getPublicKey().equals(signatureKey)) {
214                    verified = true;
215                    break;
216                }
217            }
218            if (!verified) {
219                throw new SignatureException("signature doesn't match any trusted key");
220            }
221
222            // The signature cert matches a trusted key.  Now verify that
223            // the digest in the cert matches the actual file data.
224            raf.seek(0);
225            final ProgressListener listenerForInner = listener;
226            SignerInfo verifyResult = block.verify(signerInfo, new InputStream() {
227                // The signature covers all of the OTA package except the
228                // archive comment and its 2-byte length.
229                long toRead = fileLen - commentSize - 2;
230                long soFar = 0;
231
232                int lastPercent = 0;
233                long lastPublishTime = startTimeMillis;
234
235                @Override
236                public int read() throws IOException {
237                    throw new UnsupportedOperationException();
238                }
239
240                @Override
241                public int read(byte[] b, int off, int len) throws IOException {
242                    if (soFar >= toRead) {
243                        return -1;
244                    }
245                    if (Thread.currentThread().isInterrupted()) {
246                        return -1;
247                    }
248
249                    int size = len;
250                    if (soFar + size > toRead) {
251                        size = (int)(toRead - soFar);
252                    }
253                    int read = raf.read(b, off, size);
254                    soFar += read;
255
256                    if (listenerForInner != null) {
257                        long now = System.currentTimeMillis();
258                        int p = (int)(soFar * 100 / toRead);
259                        if (p > lastPercent &&
260                            now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
261                            lastPercent = p;
262                            lastPublishTime = now;
263                            listenerForInner.onProgress(lastPercent);
264                        }
265                    }
266
267                    return read;
268                }
269            });
270
271            final boolean interrupted = Thread.interrupted();
272            if (listener != null) {
273                listener.onProgress(100);
274            }
275
276            if (interrupted) {
277                throw new SignatureException("verification was interrupted");
278            }
279
280            if (verifyResult == null) {
281                throw new SignatureException("signature digest verification failed");
282            }
283        } finally {
284            raf.close();
285        }
286    }
287
288    /**
289     * Reboots the device in order to install the given update
290     * package.
291     * Requires the {@link android.Manifest.permission#REBOOT} permission.
292     *
293     * @param context      the Context to use
294     * @param packageFile  the update package to install.  Must be on
295     * a partition mountable by recovery.  (The set of partitions
296     * known to recovery may vary from device to device.  Generally,
297     * /cache and /data are safe.)
298     *
299     * @throws IOException  if writing the recovery command file
300     * fails, or if the reboot itself fails.
301     */
302    public static void installPackage(Context context, File packageFile)
303        throws IOException {
304        String filename = packageFile.getCanonicalPath();
305
306        FileWriter uncryptFile = new FileWriter(UNCRYPT_FILE);
307        try {
308            uncryptFile.write(filename + "\n");
309        } finally {
310            uncryptFile.close();
311        }
312        // UNCRYPT_FILE needs to be readable by system server on bootup.
313        if (!UNCRYPT_FILE.setReadable(true, false)) {
314            Log.e(TAG, "Error setting readable for " + UNCRYPT_FILE.getCanonicalPath());
315        }
316        Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
317
318        // If the package is on the /data partition, write the block map file
319        // into COMMAND_FILE instead.
320        if (filename.startsWith("/data/")) {
321            filename = "@/cache/recovery/block.map";
322        }
323
324        final String filenameArg = "--update_package=" + filename;
325        final String localeArg = "--locale=" + Locale.getDefault().toString();
326        bootCommand(context, filenameArg, localeArg);
327    }
328
329    /**
330     * Reboots the device and wipes the user data and cache
331     * partitions.  This is sometimes called a "factory reset", which
332     * is something of a misnomer because the system partition is not
333     * restored to its factory state.  Requires the
334     * {@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     * @throws SecurityException if the current user is not allowed to wipe data.
341     */
342    public static void rebootWipeUserData(Context context) throws IOException {
343        rebootWipeUserData(context, false, context.getPackageName());
344    }
345
346    /** {@hide} */
347    public static void rebootWipeUserData(Context context, String reason) throws IOException {
348        rebootWipeUserData(context, false, reason);
349    }
350
351    /** {@hide} */
352    public static void rebootWipeUserData(Context context, boolean shutdown)
353            throws IOException {
354        rebootWipeUserData(context, shutdown, context.getPackageName());
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     * @param shutdown  if true, the device will be powered down after
366     *                  the wipe completes, rather than being rebooted
367     *                  back to the regular system.
368     *
369     * @throws IOException  if writing the recovery command file
370     * fails, or if the reboot itself fails.
371     * @throws SecurityException if the current user is not allowed to wipe data.
372     *
373     * @hide
374     */
375    public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
376            throws IOException {
377        UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
378        if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
379            throw new SecurityException("Wiping data is not allowed for this user.");
380        }
381        final ConditionVariable condition = new ConditionVariable();
382
383        Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
384        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
385        context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM,
386                android.Manifest.permission.MASTER_CLEAR,
387                new BroadcastReceiver() {
388                    @Override
389                    public void onReceive(Context context, Intent intent) {
390                        condition.open();
391                    }
392                }, null, 0, null, null);
393
394        // Block until the ordered broadcast has completed.
395        condition.block();
396
397        String shutdownArg = null;
398        if (shutdown) {
399            shutdownArg = "--shutdown_after";
400        }
401
402        String reasonArg = null;
403        if (!TextUtils.isEmpty(reason)) {
404            reasonArg = "--reason=" + sanitizeArg(reason);
405        }
406
407        final String localeArg = "--locale=" + Locale.getDefault().toString();
408        bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
409    }
410
411    /**
412     * Reboot into the recovery system to wipe the /cache partition.
413     * @throws IOException if something goes wrong.
414     */
415    public static void rebootWipeCache(Context context) throws IOException {
416        rebootWipeCache(context, context.getPackageName());
417    }
418
419    /** {@hide} */
420    public static void rebootWipeCache(Context context, String reason) throws IOException {
421        String reasonArg = null;
422        if (!TextUtils.isEmpty(reason)) {
423            reasonArg = "--reason=" + sanitizeArg(reason);
424        }
425
426        final String localeArg = "--locale=" + Locale.getDefault().toString();
427        bootCommand(context, "--wipe_cache", reasonArg, localeArg);
428    }
429
430    /**
431     * Reboot into the recovery system with the supplied argument.
432     * @param args to pass to the recovery utility.
433     * @throws IOException if something goes wrong.
434     */
435    private static void bootCommand(Context context, String... args) throws IOException {
436        RECOVERY_DIR.mkdirs();  // In case we need it
437        COMMAND_FILE.delete();  // In case it's not writable
438        LOG_FILE.delete();
439
440        FileWriter command = new FileWriter(COMMAND_FILE);
441        try {
442            for (String arg : args) {
443                if (!TextUtils.isEmpty(arg)) {
444                    command.write(arg);
445                    command.write("\n");
446                }
447            }
448        } finally {
449            command.close();
450        }
451
452        // Having written the command file, go ahead and reboot
453        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
454        pm.reboot(PowerManager.REBOOT_RECOVERY);
455
456        throw new IOException("Reboot failed (no permissions?)");
457    }
458
459    /**
460     * Called after booting to process and remove recovery-related files.
461     * @return the log file from recovery, or null if none was found.
462     *
463     * @hide
464     */
465    public static String handleAftermath() {
466        // Record the tail of the LOG_FILE
467        String log = null;
468        try {
469            log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
470        } catch (FileNotFoundException e) {
471            Log.i(TAG, "No recovery log file");
472        } catch (IOException e) {
473            Log.e(TAG, "Error reading recovery log", e);
474        }
475
476        if (UNCRYPT_FILE.exists()) {
477            String filename = null;
478            try {
479                filename = FileUtils.readTextFile(UNCRYPT_FILE, 0, null);
480            } catch (IOException e) {
481                Log.e(TAG, "Error reading uncrypt file", e);
482            }
483
484            // Remove the OTA package on /data that has been (possibly
485            // partially) processed. (Bug: 24973532)
486            if (filename != null && filename.startsWith("/data")) {
487                if (UNCRYPT_FILE.delete()) {
488                    Log.i(TAG, "Deleted: " + filename);
489                } else {
490                    Log.e(TAG, "Can't delete: " + filename);
491                }
492            }
493        }
494
495        // Delete everything in RECOVERY_DIR except those beginning
496        // with LAST_PREFIX
497        String[] names = RECOVERY_DIR.list();
498        for (int i = 0; names != null && i < names.length; i++) {
499            if (names[i].startsWith(LAST_PREFIX)) continue;
500            recursiveDelete(new File(RECOVERY_DIR, names[i]));
501        }
502
503        return log;
504    }
505
506    /**
507     * Internally, delete a given file or directory recursively.
508     */
509    private static void recursiveDelete(File name) {
510        if (name.isDirectory()) {
511            String[] files = name.list();
512            for (int i = 0; files != null && i < files.length; i++) {
513                File f = new File(name, files[i]);
514                recursiveDelete(f);
515            }
516        }
517
518        if (!name.delete()) {
519            Log.e(TAG, "Can't delete: " + name);
520        } else {
521            Log.i(TAG, "Deleted: " + name);
522        }
523    }
524
525    /**
526     * Internally, recovery treats each line of the command file as a separate
527     * argv, so we only need to protect against newlines and nulls.
528     */
529    private static String sanitizeArg(String arg) {
530        arg = arg.replace('\0', '?');
531        arg = arg.replace('\n', '?');
532        return arg;
533    }
534
535
536    /**
537     * @removed Was previously made visible by accident.
538     */
539    public RecoverySystem() { }
540}
541