• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.os;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.util.Log;
23 
24 import java.io.ByteArrayInputStream;
25 import java.io.File;
26 import java.io.FileNotFoundException;
27 import java.io.FileWriter;
28 import java.io.IOException;
29 import java.io.RandomAccessFile;
30 import java.security.GeneralSecurityException;
31 import java.security.PublicKey;
32 import java.security.Signature;
33 import java.security.SignatureException;
34 import java.security.cert.Certificate;
35 import java.security.cert.CertificateFactory;
36 import java.security.cert.X509Certificate;
37 import java.util.Collection;
38 import java.util.Enumeration;
39 import java.util.HashSet;
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.zip.ZipEntry;
43 import java.util.zip.ZipFile;
44 
45 import org.apache.harmony.security.asn1.BerInputStream;
46 import org.apache.harmony.security.pkcs7.ContentInfo;
47 import org.apache.harmony.security.pkcs7.SignedData;
48 import org.apache.harmony.security.pkcs7.SignerInfo;
49 import 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  */
56 public 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_LOG_FILENAME = "last_log";
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          */
onProgress(int progress)90         public void onProgress(int progress);
91     }
92 
93     /** @return the set of certs that can be used to sign an OTA package. */
getTrustedCerts(File keystore)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      */
verifyPackage(File packageFile, ProgressListener listener, File deviceCertsZipFile)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      */
installPackage(Context context, File packageFile)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      */
rebootWipeUserData(Context context)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 /data partition and toggle
361      * Encrypted File Systems on/off.
362      * @param extras to add to the RECOVERY_COMPLETED intent after rebooting.
363      * @throws IOException if something goes wrong.
364      *
365      * @hide
366      */
rebootToggleEFS(Context context, boolean efsEnabled)367     public static void rebootToggleEFS(Context context, boolean efsEnabled)
368         throws IOException {
369         if (efsEnabled) {
370             bootCommand(context, "--set_encrypted_filesystem=on");
371         } else {
372             bootCommand(context, "--set_encrypted_filesystem=off");
373         }
374     }
375 
376     /**
377      * Reboot into the recovery system with the supplied argument.
378      * @param arg to pass to the recovery utility.
379      * @throws IOException if something goes wrong.
380      */
bootCommand(Context context, String arg)381     private static void bootCommand(Context context, String arg) throws IOException {
382         RECOVERY_DIR.mkdirs();  // In case we need it
383         COMMAND_FILE.delete();  // In case it's not writable
384         LOG_FILE.delete();
385 
386         FileWriter command = new FileWriter(COMMAND_FILE);
387         try {
388             command.write(arg);
389             command.write("\n");
390         } finally {
391             command.close();
392         }
393 
394         // Having written the command file, go ahead and reboot
395         PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
396         pm.reboot("recovery");
397 
398         throw new IOException("Reboot failed (no permissions?)");
399     }
400 
401     /**
402      * Called after booting to process and remove recovery-related files.
403      * @return the log file from recovery, or null if none was found.
404      *
405      * @hide
406      */
handleAftermath()407     public static String handleAftermath() {
408         // Record the tail of the LOG_FILE
409         String log = null;
410         try {
411             log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
412         } catch (FileNotFoundException e) {
413             Log.i(TAG, "No recovery log file");
414         } catch (IOException e) {
415             Log.e(TAG, "Error reading recovery log", e);
416         }
417 
418         // Delete everything in RECOVERY_DIR except LAST_LOG_FILENAME
419         String[] names = RECOVERY_DIR.list();
420         for (int i = 0; names != null && i < names.length; i++) {
421             if (names[i].equals(LAST_LOG_FILENAME)) continue;
422             File f = new File(RECOVERY_DIR, names[i]);
423             if (!f.delete()) {
424                 Log.e(TAG, "Can't delete: " + f);
425             } else {
426                 Log.i(TAG, "Deleted: " + f);
427             }
428         }
429 
430         return log;
431     }
432 
RecoverySystem()433     private void RecoverySystem() { }  // Do not instantiate
434 }
435