RecoverySystem.java revision e2d58e95a09590a63f1c597bb808b925bcab9a69
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.Certificate;
36import java.security.cert.CertificateFactory;
37import java.security.cert.X509Certificate;
38import java.util.Collection;
39import java.util.Enumeration;
40import java.util.HashSet;
41import java.util.Iterator;
42import java.util.List;
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.provider.cert.X509CertImpl;
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<Certificate> getTrustedCerts(File keystore)
96        throws IOException, GeneralSecurityException {
97        HashSet<Certificate> trusted = new HashSet<Certificate>();
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(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            Collection 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 it = encCerts.iterator();
210            X509Certificate cert = null;
211            if (it.hasNext()) {
212                cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next());
213            } else {
214                throw new SignatureException("signature contains no certificates");
215            }
216
217            List sigInfos = signedData.getSignerInfos();
218            SignerInfo sigInfo;
219            if (!sigInfos.isEmpty()) {
220                sigInfo = (SignerInfo)sigInfos.get(0);
221            } else {
222                throw new IOException("no signer infos!");
223            }
224
225            // Check that the public key of the certificate contained
226            // in the package equals one of our trusted public keys.
227
228            HashSet<Certificate> trusted = getTrustedCerts(
229                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
230
231            PublicKey signatureKey = cert.getPublicKey();
232            boolean verified = false;
233            for (Certificate c : trusted) {
234                if (c.getPublicKey().equals(signatureKey)) {
235                    verified = true;
236                    break;
237                }
238            }
239            if (!verified) {
240                throw new SignatureException("signature doesn't match any trusted key");
241            }
242
243            // The signature cert matches a trusted key.  Now verify that
244            // the digest in the cert matches the actual file data.
245
246            // The verifier in recovery *only* handles SHA1withRSA
247            // signatures.  SignApk.java always uses SHA1withRSA, no
248            // matter what the cert says to use.  Ignore
249            // cert.getSigAlgName(), and instead use whatever
250            // algorithm is used by the signature (which should be
251            // SHA1withRSA).
252
253            String da = sigInfo.getDigestAlgorithm();
254            String dea = sigInfo.getDigestEncryptionAlgorithm();
255            String alg = null;
256            if (da == null || dea == null) {
257                // fall back to the cert algorithm if the sig one
258                // doesn't look right.
259                alg = cert.getSigAlgName();
260            } else {
261                alg = da + "with" + dea;
262            }
263            Signature sig = Signature.getInstance(alg);
264            sig.initVerify(cert);
265
266            // The signature covers all of the OTA package except the
267            // archive comment and its 2-byte length.
268            long toRead = fileLen - commentSize - 2;
269            long soFar = 0;
270            raf.seek(0);
271            byte[] buffer = new byte[4096];
272            boolean interrupted = false;
273            while (soFar < toRead) {
274                interrupted = Thread.interrupted();
275                if (interrupted) break;
276                int size = buffer.length;
277                if (soFar + size > toRead) {
278                    size = (int)(toRead - soFar);
279                }
280                int read = raf.read(buffer, 0, size);
281                sig.update(buffer, 0, read);
282                soFar += read;
283
284                if (listener != null) {
285                    long now = System.currentTimeMillis();
286                    int p = (int)(soFar * 100 / toRead);
287                    if (p > lastPercent &&
288                        now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
289                        lastPercent = p;
290                        lastPublishTime = now;
291                        listener.onProgress(lastPercent);
292                    }
293                }
294            }
295            if (listener != null) {
296                listener.onProgress(100);
297            }
298
299            if (interrupted) {
300                throw new SignatureException("verification was interrupted");
301            }
302
303            if (!sig.verify(sigInfo.getEncryptedDigest())) {
304                throw new SignatureException("signature digest verification failed");
305            }
306        } finally {
307            raf.close();
308        }
309    }
310
311    /**
312     * Reboots the device in order to install the given update
313     * package.
314     * Requires the {@link android.Manifest.permission#REBOOT} permission.
315     *
316     * @param context      the Context to use
317     * @param packageFile  the update package to install.  Must be on
318     * a partition mountable by recovery.  (The set of partitions
319     * known to recovery may vary from device to device.  Generally,
320     * /cache and /data are safe.)
321     *
322     * @throws IOException  if writing the recovery command file
323     * fails, or if the reboot itself fails.
324     */
325    public static void installPackage(Context context, File packageFile)
326        throws IOException {
327        String filename = packageFile.getCanonicalPath();
328        Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
329        String arg = "--update_package=" + filename;
330        bootCommand(context, arg);
331    }
332
333    /**
334     * Reboots the device and wipes the user data partition.  This is
335     * sometimes called a "factory reset", which is something of a
336     * misnomer because the system partition is not restored to its
337     * factory state.
338     * Requires the {@link android.Manifest.permission#REBOOT} permission.
339     *
340     * @param context  the Context to use
341     *
342     * @throws IOException  if writing the recovery command file
343     * fails, or if the reboot itself fails.
344     */
345    public static void rebootWipeUserData(Context context) throws IOException {
346        final ConditionVariable condition = new ConditionVariable();
347
348        Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
349        context.sendOrderedBroadcast(intent, android.Manifest.permission.MASTER_CLEAR,
350                new BroadcastReceiver() {
351                    @Override
352                    public void onReceive(Context context, Intent intent) {
353                        condition.open();
354                    }
355                }, null, 0, null, null);
356
357        // Block until the ordered broadcast has completed.
358        condition.block();
359
360        bootCommand(context, "--wipe_data");
361    }
362
363    /**
364     * Reboot into the recovery system to wipe the /cache partition.
365     * @throws IOException if something goes wrong.
366     */
367    public static void rebootWipeCache(Context context) throws IOException {
368        bootCommand(context, "--wipe_cache");
369    }
370
371    /**
372     * Reboot into the recovery system with the supplied argument.
373     * @param arg to pass to the recovery utility.
374     * @throws IOException if something goes wrong.
375     */
376    private static void bootCommand(Context context, String arg) throws IOException {
377        RECOVERY_DIR.mkdirs();  // In case we need it
378        COMMAND_FILE.delete();  // In case it's not writable
379        LOG_FILE.delete();
380
381        FileWriter command = new FileWriter(COMMAND_FILE);
382        try {
383            command.write(arg);
384            command.write("\n");
385        } finally {
386            command.close();
387        }
388
389        // Having written the command file, go ahead and reboot
390        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
391        pm.reboot("recovery");
392
393        throw new IOException("Reboot failed (no permissions?)");
394    }
395
396    /**
397     * Called after booting to process and remove recovery-related files.
398     * @return the log file from recovery, or null if none was found.
399     *
400     * @hide
401     */
402    public static String handleAftermath() {
403        // Record the tail of the LOG_FILE
404        String log = null;
405        try {
406            log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
407        } catch (FileNotFoundException e) {
408            Log.i(TAG, "No recovery log file");
409        } catch (IOException e) {
410            Log.e(TAG, "Error reading recovery log", e);
411        }
412
413        // Delete everything in RECOVERY_DIR except those beginning
414        // with LAST_PREFIX
415        String[] names = RECOVERY_DIR.list();
416        for (int i = 0; names != null && i < names.length; i++) {
417            if (names[i].startsWith(LAST_PREFIX)) continue;
418            File f = new File(RECOVERY_DIR, names[i]);
419            if (!f.delete()) {
420                Log.e(TAG, "Can't delete: " + f);
421            } else {
422                Log.i(TAG, "Deleted: " + f);
423            }
424        }
425
426        return log;
427    }
428
429    private void RecoverySystem() { }  // Do not instantiate
430}
431