RecoverySystem.java revision cb95657326add53f81cd2f8a0ae0a1a0527ae799
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 java.io.ByteArrayInputStream;
20import java.io.File;
21import java.io.FileNotFoundException;
22import java.io.FileWriter;
23import java.io.IOException;
24import java.io.RandomAccessFile;
25import java.security.GeneralSecurityException;
26import java.security.PublicKey;
27import java.security.Signature;
28import java.security.SignatureException;
29import java.security.cert.Certificate;
30import java.security.cert.CertificateFactory;
31import java.security.cert.X509Certificate;
32import java.util.Collection;
33import java.util.Enumeration;
34import java.util.HashSet;
35import java.util.Iterator;
36import java.util.List;
37import java.util.zip.ZipEntry;
38import java.util.zip.ZipFile;
39
40import android.content.Context;
41import android.util.Log;
42
43import org.apache.harmony.security.asn1.BerInputStream;
44import org.apache.harmony.security.pkcs7.ContentInfo;
45import org.apache.harmony.security.pkcs7.SignedData;
46import org.apache.harmony.security.pkcs7.SignerInfo;
47import org.apache.harmony.security.provider.cert.X509CertImpl;
48
49/**
50 * RecoverySystem contains methods for interacting with the Android
51 * recovery system (the separate partition that can be used to install
52 * system updates, wipe user data, etc.)
53 */
54public class RecoverySystem {
55    private static final String TAG = "RecoverySystem";
56
57    /**
58     * Default location of zip file containing public keys (X509
59     * certs) authorized to sign OTA updates.
60     */
61    private static final File DEFAULT_KEYSTORE =
62        new File("/system/etc/security/otacerts.zip");
63
64    /** Send progress to listeners no more often than this (in ms). */
65    private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
66
67    /** Used to communicate with recovery.  See bootable/recovery/recovery.c. */
68    private static File RECOVERY_DIR = new File("/cache/recovery");
69    private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
70    private static File LOG_FILE = new File(RECOVERY_DIR, "log");
71
72    // Length limits for reading files.
73    private static int LOG_FILE_MAX_LENGTH = 8 * 1024;
74
75    /**
76     * Interface definition for a callback to be invoked regularly as
77     * verification proceeds.
78     */
79    public interface ProgressListener {
80        /**
81         * Called periodically as the verification progresses.
82         *
83         * @param progress  the approximate percentage of the
84         *        verification that has been completed, ranging from 0
85         *        to 100 (inclusive).
86         */
87        public void onProgress(int progress);
88    }
89
90    /** @return the set of certs that can be used to sign an OTA package. */
91    private static HashSet<Certificate> getTrustedCerts(File keystore)
92        throws IOException, GeneralSecurityException {
93        HashSet<Certificate> trusted = new HashSet<Certificate>();
94        if (keystore == null) {
95            keystore = DEFAULT_KEYSTORE;
96        }
97        ZipFile zip = new ZipFile(keystore);
98        try {
99            CertificateFactory cf = CertificateFactory.getInstance("X.509");
100            Enumeration<? extends ZipEntry> entries = zip.entries();
101            while (entries.hasMoreElements()) {
102                ZipEntry entry = entries.nextElement();
103                trusted.add(cf.generateCertificate(zip.getInputStream(entry)));
104            }
105        } finally {
106            zip.close();
107        }
108        return trusted;
109    }
110
111    /**
112     * Verify the cryptographic signature of a system update package
113     * before installing it.  Note that the package is also verified
114     * separately by the installer once the device is rebooted into
115     * the recovery system.  This function will return only if the
116     * package was successfully verified; otherwise it will throw an
117     * exception.
118     *
119     * Verification of a package can take significant time, so this
120     * function should not be called from a UI thread.  Interrupting
121     * the thread while this function is in progress will result in a
122     * SecurityException being thrown (and the thread's interrupt flag
123     * will be cleared).
124     *
125     * @param packageFile  the package to be verified
126     * @param listener     an object to receive periodic progress
127     * updates as verification proceeds.  May be null.
128     * @param deviceCertsZipFile  the zip file of certificates whose
129     * public keys we will accept.  Verification succeeds if the
130     * package is signed by the private key corresponding to any
131     * public key in this file.  May be null to use the system default
132     * file (currently "/system/etc/security/otacerts.zip").
133     *
134     * @throws IOException if there were any errors reading the
135     * package or certs files.
136     * @throws GeneralSecurityException if verification failed
137     */
138    public static void verifyPackage(File packageFile,
139                                     ProgressListener listener,
140                                     File deviceCertsZipFile)
141        throws IOException, GeneralSecurityException {
142        long fileLen = packageFile.length();
143
144        RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
145        try {
146            int lastPercent = 0;
147            long lastPublishTime = System.currentTimeMillis();
148            if (listener != null) {
149                listener.onProgress(lastPercent);
150            }
151
152            raf.seek(fileLen - 6);
153            byte[] footer = new byte[6];
154            raf.readFully(footer);
155
156            if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
157                throw new SignatureException("no signature in file (no footer)");
158            }
159
160            int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
161            int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
162            Log.v(TAG, String.format("comment size %d; signature start %d",
163                                     commentSize, signatureStart));
164
165            byte[] eocd = new byte[commentSize + 22];
166            raf.seek(fileLen - (commentSize + 22));
167            raf.readFully(eocd);
168
169            // Check that we have found the start of the
170            // end-of-central-directory record.
171            if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
172                eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
173                throw new SignatureException("no signature in file (bad footer)");
174            }
175
176            for (int i = 4; i < eocd.length-3; ++i) {
177                if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
178                    eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
179                    throw new SignatureException("EOCD marker found after start of EOCD");
180                }
181            }
182
183            // The following code is largely copied from
184            // JarUtils.verifySignature().  We could just *call* that
185            // method here if that function didn't read the entire
186            // input (ie, the whole OTA package) into memory just to
187            // compute its message digest.
188
189            BerInputStream bis = new BerInputStream(
190                new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
191            ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
192            SignedData signedData = info.getSignedData();
193            if (signedData == null) {
194                throw new IOException("signedData is null");
195            }
196            Collection encCerts = signedData.getCertificates();
197            if (encCerts.isEmpty()) {
198                throw new IOException("encCerts is empty");
199            }
200            // Take the first certificate from the signature (packages
201            // should contain only one).
202            Iterator it = encCerts.iterator();
203            X509Certificate cert = null;
204            if (it.hasNext()) {
205                cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next());
206            } else {
207                throw new SignatureException("signature contains no certificates");
208            }
209
210            List sigInfos = signedData.getSignerInfos();
211            SignerInfo sigInfo;
212            if (!sigInfos.isEmpty()) {
213                sigInfo = (SignerInfo)sigInfos.get(0);
214            } else {
215                throw new IOException("no signer infos!");
216            }
217
218            // Check that the public key of the certificate contained
219            // in the package equals one of our trusted public keys.
220
221            HashSet<Certificate> trusted = getTrustedCerts(
222                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
223
224            PublicKey signatureKey = cert.getPublicKey();
225            boolean verified = false;
226            for (Certificate c : trusted) {
227                if (c.getPublicKey().equals(signatureKey)) {
228                    verified = true;
229                    break;
230                }
231            }
232            if (!verified) {
233                throw new SignatureException("signature doesn't match any trusted key");
234            }
235
236            // The signature cert matches a trusted key.  Now verify that
237            // the digest in the cert matches the actual file data.
238
239            // The verifier in recovery *only* handles SHA1withRSA
240            // signatures.  SignApk.java always uses SHA1withRSA, no
241            // matter what the cert says to use.  Ignore
242            // cert.getSigAlgName(), and instead use whatever
243            // algorithm is used by the signature (which should be
244            // SHA1withRSA).
245
246            String da = sigInfo.getdigestAlgorithm();
247            String dea = sigInfo.getDigestEncryptionAlgorithm();
248            String alg = null;
249            if (da == null || dea == null) {
250                // fall back to the cert algorithm if the sig one
251                // doesn't look right.
252                alg = cert.getSigAlgName();
253            } else {
254                alg = da + "with" + dea;
255            }
256            Signature sig = Signature.getInstance(alg);
257            sig.initVerify(cert);
258
259            // The signature covers all of the OTA package except the
260            // archive comment and its 2-byte length.
261            long toRead = fileLen - commentSize - 2;
262            long soFar = 0;
263            raf.seek(0);
264            byte[] buffer = new byte[4096];
265            boolean interrupted = false;
266            while (soFar < toRead) {
267                interrupted = Thread.interrupted();
268                if (interrupted) break;
269                int size = buffer.length;
270                if (soFar + size > toRead) {
271                    size = (int)(toRead - soFar);
272                }
273                int read = raf.read(buffer, 0, size);
274                sig.update(buffer, 0, read);
275                soFar += read;
276
277                if (listener != null) {
278                    long now = System.currentTimeMillis();
279                    int p = (int)(soFar * 100 / toRead);
280                    if (p > lastPercent &&
281                        now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
282                        lastPercent = p;
283                        lastPublishTime = now;
284                        listener.onProgress(lastPercent);
285                    }
286                }
287            }
288            if (listener != null) {
289                listener.onProgress(100);
290            }
291
292            if (interrupted) {
293                throw new SignatureException("verification was interrupted");
294            }
295
296            if (!sig.verify(sigInfo.getEncryptedDigest())) {
297                throw new SignatureException("signature digest verification failed");
298            }
299        } finally {
300            raf.close();
301        }
302    }
303
304    /**
305     * Reboots the device in order to install the given update
306     * package.
307     * Requires the {@link android.Manifest.permission#REBOOT}
308     * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM}
309     * permissions.
310     *
311     * @param context      the Context to use
312     * @param packageFile  the update package to install.  Currently
313     * must be on the /cache or /data partitions.
314     *
315     * @throws IOException  if writing the recovery command file
316     * fails, or if the reboot itself fails.
317     */
318    public static void installPackage(Context context, File packageFile)
319        throws IOException {
320        String filename = packageFile.getCanonicalPath();
321
322        if (filename.startsWith("/cache/")) {
323            filename = "CACHE:" + filename.substring(7);
324        } else if (filename.startsWith("/data/")) {
325            filename = "DATA:" + filename.substring(6);
326        } else {
327            throw new IllegalArgumentException(
328                "Must start with /cache or /data: " + filename);
329        }
330        Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
331        String arg = "--update_package=" + filename;
332        bootCommand(context, arg);
333    }
334
335    /**
336     * Reboots the device and wipes the user data partition.  This is
337     * sometimes called a "factory reset", which is something of a
338     * misnomer because the system partition is not restored to its
339     * factory state.
340     * Requires the {@link android.Manifest.permission#REBOOT}
341     * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM}
342     * permissions.
343     *
344     * @param context  the Context to use
345     *
346     * @throws IOException  if writing the recovery command file
347     * fails, or if the reboot itself fails.
348     */
349    public static void rebootWipeUserData(Context context)
350        throws IOException {
351        bootCommand(context, "--wipe_data");
352    }
353
354    /**
355     * Reboot into the recovery system to wipe the /data partition and toggle
356     * Encrypted File Systems on/off.
357     * @param extras to add to the RECOVERY_COMPLETED intent after rebooting.
358     * @throws IOException if something goes wrong.
359     *
360     * @hide
361     */
362    public static void rebootToggleEFS(Context context, boolean efsEnabled)
363        throws IOException {
364        if (efsEnabled) {
365            bootCommand(context, "--set_encrypted_filesystem=on");
366        } else {
367            bootCommand(context, "--set_encrypted_filesystem=off");
368        }
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
414        String[] names = RECOVERY_DIR.list();
415        for (int i = 0; names != null && i < names.length; i++) {
416            File f = new File(RECOVERY_DIR, names[i]);
417            if (!f.delete()) {
418                Log.e(TAG, "Can't delete: " + f);
419            } else {
420                Log.i(TAG, "Deleted: " + f);
421            }
422        }
423
424        return log;
425    }
426
427    private void RecoverySystem() { }  // Do not instantiate
428}
429