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