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.RandomAccessFile;
30import java.security.GeneralSecurityException;
31import java.security.PublicKey;
32import java.security.Signature;
33import java.security.SignatureException;
34import java.security.cert.Certificate;
35import java.security.cert.CertificateFactory;
36import java.security.cert.X509Certificate;
37import java.util.Collection;
38import java.util.Enumeration;
39import java.util.HashSet;
40import java.util.Iterator;
41import java.util.List;
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.provider.cert.X509CertImpl;
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<Certificate> getTrustedCerts(File keystore)
95        throws IOException, GeneralSecurityException {
96        HashSet<Certificate> trusted = new HashSet<Certificate>();
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                trusted.add(cf.generateCertificate(zip.getInputStream(entry)));
107            }
108        } finally {
109            zip.close();
110        }
111        return trusted;
112    }
113
114    /**
115     * Verify the cryptographic signature of a system update package
116     * before installing it.  Note that the package is also verified
117     * separately by the installer once the device is rebooted into
118     * the recovery system.  This function will return only if the
119     * package was successfully verified; otherwise it will throw an
120     * exception.
121     *
122     * Verification of a package can take significant time, so this
123     * function should not be called from a UI thread.  Interrupting
124     * the thread while this function is in progress will result in a
125     * SecurityException being thrown (and the thread's interrupt flag
126     * will be cleared).
127     *
128     * @param packageFile  the package to be verified
129     * @param listener     an object to receive periodic progress
130     * updates as verification proceeds.  May be null.
131     * @param deviceCertsZipFile  the zip file of certificates whose
132     * public keys we will accept.  Verification succeeds if the
133     * package is signed by the private key corresponding to any
134     * public key in this file.  May be null to use the system default
135     * file (currently "/system/etc/security/otacerts.zip").
136     *
137     * @throws IOException if there were any errors reading the
138     * package or certs files.
139     * @throws GeneralSecurityException if verification failed
140     */
141    public static void verifyPackage(File packageFile,
142                                     ProgressListener listener,
143                                     File deviceCertsZipFile)
144        throws IOException, GeneralSecurityException {
145        long fileLen = packageFile.length();
146
147        RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
148        try {
149            int lastPercent = 0;
150            long lastPublishTime = System.currentTimeMillis();
151            if (listener != null) {
152                listener.onProgress(lastPercent);
153            }
154
155            raf.seek(fileLen - 6);
156            byte[] footer = new byte[6];
157            raf.readFully(footer);
158
159            if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
160                throw new SignatureException("no signature in file (no footer)");
161            }
162
163            int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
164            int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
165            Log.v(TAG, String.format("comment size %d; signature start %d",
166                                     commentSize, signatureStart));
167
168            byte[] eocd = new byte[commentSize + 22];
169            raf.seek(fileLen - (commentSize + 22));
170            raf.readFully(eocd);
171
172            // Check that we have found the start of the
173            // end-of-central-directory record.
174            if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
175                eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
176                throw new SignatureException("no signature in file (bad footer)");
177            }
178
179            for (int i = 4; i < eocd.length-3; ++i) {
180                if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
181                    eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
182                    throw new SignatureException("EOCD marker found after start of EOCD");
183                }
184            }
185
186            // The following code is largely copied from
187            // JarUtils.verifySignature().  We could just *call* that
188            // method here if that function didn't read the entire
189            // input (ie, the whole OTA package) into memory just to
190            // compute its message digest.
191
192            BerInputStream bis = new BerInputStream(
193                new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
194            ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
195            SignedData signedData = info.getSignedData();
196            if (signedData == null) {
197                throw new IOException("signedData is null");
198            }
199            Collection encCerts = signedData.getCertificates();
200            if (encCerts.isEmpty()) {
201                throw new IOException("encCerts is empty");
202            }
203            // Take the first certificate from the signature (packages
204            // should contain only one).
205            Iterator it = encCerts.iterator();
206            X509Certificate cert = null;
207            if (it.hasNext()) {
208                cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next());
209            } else {
210                throw new SignatureException("signature contains no certificates");
211            }
212
213            List sigInfos = signedData.getSignerInfos();
214            SignerInfo sigInfo;
215            if (!sigInfos.isEmpty()) {
216                sigInfo = (SignerInfo)sigInfos.get(0);
217            } else {
218                throw new IOException("no signer infos!");
219            }
220
221            // Check that the public key of the certificate contained
222            // in the package equals one of our trusted public keys.
223
224            HashSet<Certificate> trusted = getTrustedCerts(
225                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
226
227            PublicKey signatureKey = cert.getPublicKey();
228            boolean verified = false;
229            for (Certificate c : trusted) {
230                if (c.getPublicKey().equals(signatureKey)) {
231                    verified = true;
232                    break;
233                }
234            }
235            if (!verified) {
236                throw new SignatureException("signature doesn't match any trusted key");
237            }
238
239            // The signature cert matches a trusted key.  Now verify that
240            // the digest in the cert matches the actual file data.
241
242            // The verifier in recovery *only* handles SHA1withRSA
243            // signatures.  SignApk.java always uses SHA1withRSA, no
244            // matter what the cert says to use.  Ignore
245            // cert.getSigAlgName(), and instead use whatever
246            // algorithm is used by the signature (which should be
247            // SHA1withRSA).
248
249            String da = sigInfo.getDigestAlgorithm();
250            String dea = sigInfo.getDigestEncryptionAlgorithm();
251            String alg = null;
252            if (da == null || dea == null) {
253                // fall back to the cert algorithm if the sig one
254                // doesn't look right.
255                alg = cert.getSigAlgName();
256            } else {
257                alg = da + "with" + dea;
258            }
259            Signature sig = Signature.getInstance(alg);
260            sig.initVerify(cert);
261
262            // The signature covers all of the OTA package except the
263            // archive comment and its 2-byte length.
264            long toRead = fileLen - commentSize - 2;
265            long soFar = 0;
266            raf.seek(0);
267            byte[] buffer = new byte[4096];
268            boolean interrupted = false;
269            while (soFar < toRead) {
270                interrupted = Thread.interrupted();
271                if (interrupted) break;
272                int size = buffer.length;
273                if (soFar + size > toRead) {
274                    size = (int)(toRead - soFar);
275                }
276                int read = raf.read(buffer, 0, size);
277                sig.update(buffer, 0, read);
278                soFar += read;
279
280                if (listener != null) {
281                    long now = System.currentTimeMillis();
282                    int p = (int)(soFar * 100 / toRead);
283                    if (p > lastPercent &&
284                        now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
285                        lastPercent = p;
286                        lastPublishTime = now;
287                        listener.onProgress(lastPercent);
288                    }
289                }
290            }
291            if (listener != null) {
292                listener.onProgress(100);
293            }
294
295            if (interrupted) {
296                throw new SignatureException("verification was interrupted");
297            }
298
299            if (!sig.verify(sigInfo.getEncryptedDigest())) {
300                throw new SignatureException("signature digest verification failed");
301            }
302        } finally {
303            raf.close();
304        }
305    }
306
307    /**
308     * Reboots the device in order to install the given update
309     * package.
310     * Requires the {@link android.Manifest.permission#REBOOT} permission.
311     *
312     * @param context      the Context to use
313     * @param packageFile  the update package to install.  Must be on
314     * a partition mountable by recovery.  (The set of partitions
315     * known to recovery may vary from device to device.  Generally,
316     * /cache and /data are safe.)
317     *
318     * @throws IOException  if writing the recovery command file
319     * fails, or if the reboot itself fails.
320     */
321    public static void installPackage(Context context, File packageFile)
322        throws IOException {
323        String filename = packageFile.getCanonicalPath();
324        Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
325        String arg = "--update_package=" + filename;
326        bootCommand(context, arg);
327    }
328
329    /**
330     * Reboots the device and wipes the user data partition.  This is
331     * sometimes called a "factory reset", which is something of a
332     * misnomer because the system partition is not restored to its
333     * factory state.
334     * Requires the {@link android.Manifest.permission#REBOOT} permission.
335     *
336     * @param context  the Context to use
337     *
338     * @throws IOException  if writing the recovery command file
339     * fails, or if the reboot itself fails.
340     */
341    public static void rebootWipeUserData(Context context) throws IOException {
342        final ConditionVariable condition = new ConditionVariable();
343
344        Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
345        context.sendOrderedBroadcast(intent, android.Manifest.permission.MASTER_CLEAR,
346                new BroadcastReceiver() {
347                    @Override
348                    public void onReceive(Context context, Intent intent) {
349                        condition.open();
350                    }
351                }, null, 0, null, null);
352
353        // Block until the ordered broadcast has completed.
354        condition.block();
355
356        bootCommand(context, "--wipe_data");
357    }
358
359    /**
360     * Reboot into the recovery system to wipe the /cache partition.
361     * @throws IOException if something goes wrong.
362     */
363    public static void rebootWipeCache(Context context) throws IOException {
364        bootCommand(context, "--wipe_cache");
365    }
366
367    /**
368     * Reboot into the recovery system with the supplied argument.
369     * @param arg to pass to the recovery utility.
370     * @throws IOException if something goes wrong.
371     */
372    private static void bootCommand(Context context, String arg) throws IOException {
373        RECOVERY_DIR.mkdirs();  // In case we need it
374        COMMAND_FILE.delete();  // In case it's not writable
375        LOG_FILE.delete();
376
377        FileWriter command = new FileWriter(COMMAND_FILE);
378        try {
379            command.write(arg);
380            command.write("\n");
381        } finally {
382            command.close();
383        }
384
385        // Having written the command file, go ahead and reboot
386        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
387        pm.reboot("recovery");
388
389        throw new IOException("Reboot failed (no permissions?)");
390    }
391
392    /**
393     * Called after booting to process and remove recovery-related files.
394     * @return the log file from recovery, or null if none was found.
395     *
396     * @hide
397     */
398    public static String handleAftermath() {
399        // Record the tail of the LOG_FILE
400        String log = null;
401        try {
402            log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
403        } catch (FileNotFoundException e) {
404            Log.i(TAG, "No recovery log file");
405        } catch (IOException e) {
406            Log.e(TAG, "Error reading recovery log", e);
407        }
408
409        // Delete everything in RECOVERY_DIR except those beginning
410        // with LAST_PREFIX
411        String[] names = RECOVERY_DIR.list();
412        for (int i = 0; names != null && i < names.length; i++) {
413            if (names[i].startsWith(LAST_PREFIX)) continue;
414            File f = new File(RECOVERY_DIR, names[i]);
415            if (!f.delete()) {
416                Log.e(TAG, "Can't delete: " + f);
417            } else {
418                Log.i(TAG, "Deleted: " + f);
419            }
420        }
421
422        return log;
423    }
424
425    private void RecoverySystem() { }  // Do not instantiate
426}
427