RecoverySystem.java revision 004a4b20f8d3116e6a711525960d433fcfea4ee4
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 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         */
92        public void onProgress(int progress);
93    }
94
95    /** @return the set of certs that can be used to sign an OTA package. */
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     */
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     */
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     */
356    public static void rebootWipeUserData(Context context) throws IOException {
357        rebootWipeUserData(context, false, context.getPackageName());
358    }
359
360    /** {@hide} */
361    public static void rebootWipeUserData(Context context, String reason) throws IOException {
362        rebootWipeUserData(context, false, reason);
363    }
364
365    /** {@hide} */
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     */
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     */
429    public static void rebootWipeCache(Context context) throws IOException {
430        rebootWipeCache(context, context.getPackageName());
431    }
432
433    /** {@hide} */
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     */
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     */
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     */
510    private static String sanitizeArg(String arg) {
511        arg = arg.replace('\0', '?');
512        arg = arg.replace('\n', '?');
513        return arg;
514    }
515
516    private void RecoverySystem() { }  // Do not instantiate
517}
518