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