RecoverySystem.java revision 183415e521d599ca5e33e5022fec5ec7dfe1c055
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.util.Log;
24
25import java.io.ByteArrayInputStream;
26import java.io.File;
27import java.io.FileNotFoundException;
28import java.io.FileWriter;
29import java.io.IOException;
30import java.io.InputStream;
31import java.io.RandomAccessFile;
32import java.security.GeneralSecurityException;
33import java.security.PublicKey;
34import java.security.Signature;
35import java.security.SignatureException;
36import java.security.cert.CertificateFactory;
37import java.security.cert.X509Certificate;
38import java.util.Enumeration;
39import java.util.HashSet;
40import java.util.Iterator;
41import java.util.List;
42import java.util.Locale;
43import java.util.zip.ZipEntry;
44import java.util.zip.ZipFile;
45
46import org.apache.harmony.security.asn1.BerInputStream;
47import org.apache.harmony.security.pkcs7.ContentInfo;
48import org.apache.harmony.security.pkcs7.SignedData;
49import org.apache.harmony.security.pkcs7.SignerInfo;
50import org.apache.harmony.security.x509.Certificate;
51
52/**
53 * RecoverySystem contains methods for interacting with the Android
54 * recovery system (the separate partition that can be used to install
55 * system updates, wipe user data, etc.)
56 */
57public class RecoverySystem {
58    private static final String TAG = "RecoverySystem";
59
60    /**
61     * Default location of zip file containing public keys (X509
62     * certs) authorized to sign OTA updates.
63     */
64    private static final File DEFAULT_KEYSTORE =
65        new File("/system/etc/security/otacerts.zip");
66
67    /** Send progress to listeners no more often than this (in ms). */
68    private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
69
70    /** Used to communicate with recovery.  See bootable/recovery/recovery.c. */
71    private static File RECOVERY_DIR = new File("/cache/recovery");
72    private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
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        long fileLen = packageFile.length();
152
153        RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
154        try {
155            int lastPercent = 0;
156            long lastPublishTime = System.currentTimeMillis();
157            if (listener != null) {
158                listener.onProgress(lastPercent);
159            }
160
161            raf.seek(fileLen - 6);
162            byte[] footer = new byte[6];
163            raf.readFully(footer);
164
165            if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
166                throw new SignatureException("no signature in file (no footer)");
167            }
168
169            int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
170            int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
171
172            byte[] eocd = new byte[commentSize + 22];
173            raf.seek(fileLen - (commentSize + 22));
174            raf.readFully(eocd);
175
176            // Check that we have found the start of the
177            // end-of-central-directory record.
178            if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
179                eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
180                throw new SignatureException("no signature in file (bad footer)");
181            }
182
183            for (int i = 4; i < eocd.length-3; ++i) {
184                if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
185                    eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
186                    throw new SignatureException("EOCD marker found after start of EOCD");
187                }
188            }
189
190            // The following code is largely copied from
191            // JarUtils.verifySignature().  We could just *call* that
192            // method here if that function didn't read the entire
193            // input (ie, the whole OTA package) into memory just to
194            // compute its message digest.
195
196            BerInputStream bis = new BerInputStream(
197                new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
198            ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
199            SignedData signedData = info.getSignedData();
200            if (signedData == null) {
201                throw new IOException("signedData is null");
202            }
203            List<Certificate> encCerts = signedData.getCertificates();
204            if (encCerts.isEmpty()) {
205                throw new IOException("encCerts is empty");
206            }
207            // Take the first certificate from the signature (packages
208            // should contain only one).
209            Iterator<Certificate> it = encCerts.iterator();
210            X509Certificate cert = null;
211            if (it.hasNext()) {
212                CertificateFactory cf = CertificateFactory.getInstance("X.509");
213                InputStream is = new ByteArrayInputStream(it.next().getEncoded());
214                cert = (X509Certificate) cf.generateCertificate(is);
215            } else {
216                throw new SignatureException("signature contains no certificates");
217            }
218
219            List<SignerInfo> sigInfos = signedData.getSignerInfos();
220            SignerInfo sigInfo;
221            if (!sigInfos.isEmpty()) {
222                sigInfo = (SignerInfo)sigInfos.get(0);
223            } else {
224                throw new IOException("no signer infos!");
225            }
226
227            // Check that the public key of the certificate contained
228            // in the package equals one of our trusted public keys.
229
230            HashSet<X509Certificate> trusted = getTrustedCerts(
231                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
232
233            PublicKey signatureKey = cert.getPublicKey();
234            boolean verified = false;
235            for (X509Certificate c : trusted) {
236                if (c.getPublicKey().equals(signatureKey)) {
237                    verified = true;
238                    break;
239                }
240            }
241            if (!verified) {
242                throw new SignatureException("signature doesn't match any trusted key");
243            }
244
245            // The signature cert matches a trusted key.  Now verify that
246            // the digest in the cert matches the actual file data.
247
248            // The verifier in recovery only handles SHA1withRSA and
249            // SHA256withRSA signatures.  SignApk chooses which to use
250            // based on the signature algorithm of the cert:
251            //
252            //    "SHA256withRSA" cert -> "SHA256withRSA" signature
253            //    "SHA1withRSA" cert   -> "SHA1withRSA" signature
254            //    "MD5withRSA" cert    -> "SHA1withRSA" signature (for backwards compatibility)
255            //    any other cert       -> SignApk fails
256            //
257            // Here we ignore whatever the cert says, and instead use
258            // whatever algorithm is used by the signature.
259
260            String da = sigInfo.getDigestAlgorithm();
261            String dea = sigInfo.getDigestEncryptionAlgorithm();
262            String alg = null;
263            if (da == null || dea == null) {
264                // fall back to the cert algorithm if the sig one
265                // doesn't look right.
266                alg = cert.getSigAlgName();
267            } else {
268                alg = da + "with" + dea;
269            }
270            Signature sig = Signature.getInstance(alg);
271            sig.initVerify(cert);
272
273            // The signature covers all of the OTA package except the
274            // archive comment and its 2-byte length.
275            long toRead = fileLen - commentSize - 2;
276            long soFar = 0;
277            raf.seek(0);
278            byte[] buffer = new byte[4096];
279            boolean interrupted = false;
280            while (soFar < toRead) {
281                interrupted = Thread.interrupted();
282                if (interrupted) break;
283                int size = buffer.length;
284                if (soFar + size > toRead) {
285                    size = (int)(toRead - soFar);
286                }
287                int read = raf.read(buffer, 0, size);
288                sig.update(buffer, 0, read);
289                soFar += read;
290
291                if (listener != null) {
292                    long now = System.currentTimeMillis();
293                    int p = (int)(soFar * 100 / toRead);
294                    if (p > lastPercent &&
295                        now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
296                        lastPercent = p;
297                        lastPublishTime = now;
298                        listener.onProgress(lastPercent);
299                    }
300                }
301            }
302            if (listener != null) {
303                listener.onProgress(100);
304            }
305
306            if (interrupted) {
307                throw new SignatureException("verification was interrupted");
308            }
309
310            if (!sig.verify(sigInfo.getEncryptedDigest())) {
311                throw new SignatureException("signature digest verification failed");
312            }
313        } finally {
314            raf.close();
315        }
316    }
317
318    /**
319     * Reboots the device in order to install the given update
320     * package.
321     * Requires the {@link android.Manifest.permission#REBOOT} permission.
322     *
323     * @param context      the Context to use
324     * @param packageFile  the update package to install.  Must be on
325     * a partition mountable by recovery.  (The set of partitions
326     * known to recovery may vary from device to device.  Generally,
327     * /cache and /data are safe.)
328     *
329     * @throws IOException  if writing the recovery command file
330     * fails, or if the reboot itself fails.
331     */
332    public static void installPackage(Context context, File packageFile)
333        throws IOException {
334        String filename = packageFile.getCanonicalPath();
335        Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
336        String arg = "--update_package=" + filename +
337            "\n--locale=" + Locale.getDefault().toString();
338        bootCommand(context, arg);
339    }
340
341    /**
342     * Reboots the device and wipes the user data and cache
343     * partitions.  This is sometimes called a "factory reset", which
344     * is something of a misnomer because the system partition is not
345     * restored to its factory state.  Requires the
346     * {@link android.Manifest.permission#REBOOT} permission.
347     *
348     * @param context  the Context to use
349     *
350     * @throws IOException  if writing the recovery command file
351     * fails, or if the reboot itself fails.
352     * @throws SecurityException if the current user is not allowed to wipe data.
353     */
354    public static void rebootWipeUserData(Context context) throws IOException {
355        rebootWipeUserData(context, false);
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)
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.OWNER,
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 = "";
399        if (shutdown) {
400            shutdownArg = "--shutdown_after\n";
401        }
402
403        bootCommand(context, shutdownArg + "--wipe_data\n--locale=" +
404                    Locale.getDefault().toString());
405    }
406
407    /**
408     * Reboot into the recovery system to wipe the /cache partition.
409     * @throws IOException if something goes wrong.
410     */
411    public static void rebootWipeCache(Context context) throws IOException {
412        bootCommand(context, "--wipe_cache\n--locale=" + Locale.getDefault().toString());
413    }
414
415    /**
416     * Reboot into the recovery system with the supplied argument.
417     * @param arg to pass to the recovery utility.
418     * @throws IOException if something goes wrong.
419     */
420    private static void bootCommand(Context context, String arg) throws IOException {
421        RECOVERY_DIR.mkdirs();  // In case we need it
422        COMMAND_FILE.delete();  // In case it's not writable
423        LOG_FILE.delete();
424
425        FileWriter command = new FileWriter(COMMAND_FILE);
426        try {
427            command.write(arg);
428            command.write("\n");
429        } finally {
430            command.close();
431        }
432
433        // Having written the command file, go ahead and reboot
434        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
435        pm.reboot(PowerManager.REBOOT_RECOVERY);
436
437        throw new IOException("Reboot failed (no permissions?)");
438    }
439
440    /**
441     * Called after booting to process and remove recovery-related files.
442     * @return the log file from recovery, or null if none was found.
443     *
444     * @hide
445     */
446    public static String handleAftermath() {
447        // Record the tail of the LOG_FILE
448        String log = null;
449        try {
450            log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
451        } catch (FileNotFoundException e) {
452            Log.i(TAG, "No recovery log file");
453        } catch (IOException e) {
454            Log.e(TAG, "Error reading recovery log", e);
455        }
456
457        // Delete everything in RECOVERY_DIR except those beginning
458        // with LAST_PREFIX
459        String[] names = RECOVERY_DIR.list();
460        for (int i = 0; names != null && i < names.length; i++) {
461            if (names[i].startsWith(LAST_PREFIX)) continue;
462            File f = new File(RECOVERY_DIR, names[i]);
463            if (!f.delete()) {
464                Log.e(TAG, "Can't delete: " + f);
465            } else {
466                Log.i(TAG, "Deleted: " + f);
467            }
468        }
469
470        return log;
471    }
472
473    private void RecoverySystem() { }  // Do not instantiate
474}
475