blob: 480fe7d56964a463ff7a1aa8b92c19c95296c0d2 [file] [log] [blame]
Doug Zongker1af33d02010-01-05 11:28:55 -08001/*
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
Jason parks4ca74dc2011-03-14 15:23:31 -050019import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.util.Log;
23
Doug Zongker1af33d02010-01-05 11:28:55 -080024import java.io.ByteArrayInputStream;
25import java.io.File;
26import java.io.FileNotFoundException;
27import java.io.FileWriter;
28import java.io.IOException;
Doug Zongkere2d58e92011-11-04 13:39:47 -070029import java.io.InputStream;
Doug Zongker1af33d02010-01-05 11:28:55 -080030import 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;
Doug Zongkere33b4002012-08-23 09:52:14 -070043import java.util.Locale;
Doug Zongker1af33d02010-01-05 11:28:55 -080044import java.util.zip.ZipEntry;
45import java.util.zip.ZipFile;
46
Doug Zongker1af33d02010-01-05 11:28:55 -080047import org.apache.harmony.security.asn1.BerInputStream;
48import org.apache.harmony.security.pkcs7.ContentInfo;
49import org.apache.harmony.security.pkcs7.SignedData;
50import org.apache.harmony.security.pkcs7.SignerInfo;
51import org.apache.harmony.security.provider.cert.X509CertImpl;
52
53/**
54 * RecoverySystem contains methods for interacting with the Android
55 * recovery system (the separate partition that can be used to install
56 * system updates, wipe user data, etc.)
Doug Zongker1af33d02010-01-05 11:28:55 -080057 */
58public class RecoverySystem {
59 private static final String TAG = "RecoverySystem";
60
61 /**
62 * Default location of zip file containing public keys (X509
63 * certs) authorized to sign OTA updates.
64 */
65 private static final File DEFAULT_KEYSTORE =
66 new File("/system/etc/security/otacerts.zip");
67
68 /** Send progress to listeners no more often than this (in ms). */
69 private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500;
70
71 /** Used to communicate with recovery. See bootable/recovery/recovery.c. */
72 private static File RECOVERY_DIR = new File("/cache/recovery");
73 private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
74 private static File LOG_FILE = new File(RECOVERY_DIR, "log");
Doug Zongker3d5040f82011-04-12 09:23:51 -070075 private static String LAST_PREFIX = "last_";
Doug Zongker1af33d02010-01-05 11:28:55 -080076
77 // Length limits for reading files.
Dan Egnorc95142d2010-03-11 12:31:23 -080078 private static int LOG_FILE_MAX_LENGTH = 64 * 1024;
Doug Zongker1af33d02010-01-05 11:28:55 -080079
80 /**
81 * Interface definition for a callback to be invoked regularly as
82 * verification proceeds.
83 */
84 public interface ProgressListener {
85 /**
86 * Called periodically as the verification progresses.
87 *
88 * @param progress the approximate percentage of the
89 * verification that has been completed, ranging from 0
90 * to 100 (inclusive).
91 */
92 public void onProgress(int progress);
93 }
94
95 /** @return the set of certs that can be used to sign an OTA package. */
96 private static HashSet<Certificate> getTrustedCerts(File keystore)
97 throws IOException, GeneralSecurityException {
98 HashSet<Certificate> trusted = new HashSet<Certificate>();
99 if (keystore == null) {
100 keystore = DEFAULT_KEYSTORE;
101 }
102 ZipFile zip = new ZipFile(keystore);
103 try {
104 CertificateFactory cf = CertificateFactory.getInstance("X.509");
105 Enumeration<? extends ZipEntry> entries = zip.entries();
106 while (entries.hasMoreElements()) {
107 ZipEntry entry = entries.nextElement();
Doug Zongkere2d58e92011-11-04 13:39:47 -0700108 InputStream is = zip.getInputStream(entry);
109 try {
110 trusted.add(cf.generateCertificate(is));
111 } finally {
112 is.close();
113 }
Doug Zongker1af33d02010-01-05 11:28:55 -0800114 }
115 } finally {
116 zip.close();
117 }
118 return trusted;
119 }
120
121 /**
122 * Verify the cryptographic signature of a system update package
123 * before installing it. Note that the package is also verified
124 * separately by the installer once the device is rebooted into
125 * the recovery system. This function will return only if the
126 * package was successfully verified; otherwise it will throw an
127 * exception.
128 *
129 * Verification of a package can take significant time, so this
Doug Zongkercb956572010-03-11 10:27:21 -0800130 * function should not be called from a UI thread. Interrupting
131 * the thread while this function is in progress will result in a
132 * SecurityException being thrown (and the thread's interrupt flag
133 * will be cleared).
Doug Zongker1af33d02010-01-05 11:28:55 -0800134 *
135 * @param packageFile the package to be verified
136 * @param listener an object to receive periodic progress
137 * updates as verification proceeds. May be null.
138 * @param deviceCertsZipFile the zip file of certificates whose
139 * public keys we will accept. Verification succeeds if the
140 * package is signed by the private key corresponding to any
141 * public key in this file. May be null to use the system default
142 * file (currently "/system/etc/security/otacerts.zip").
143 *
144 * @throws IOException if there were any errors reading the
145 * package or certs files.
146 * @throws GeneralSecurityException if verification failed
147 */
148 public static void verifyPackage(File packageFile,
149 ProgressListener listener,
150 File deviceCertsZipFile)
151 throws IOException, GeneralSecurityException {
152 long fileLen = packageFile.length();
153
154 RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
155 try {
156 int lastPercent = 0;
157 long lastPublishTime = System.currentTimeMillis();
158 if (listener != null) {
159 listener.onProgress(lastPercent);
160 }
161
162 raf.seek(fileLen - 6);
163 byte[] footer = new byte[6];
164 raf.readFully(footer);
165
166 if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
167 throw new SignatureException("no signature in file (no footer)");
168 }
169
170 int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
171 int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);
Doug Zongker1af33d02010-01-05 11:28:55 -0800172
173 byte[] eocd = new byte[commentSize + 22];
174 raf.seek(fileLen - (commentSize + 22));
175 raf.readFully(eocd);
176
177 // Check that we have found the start of the
178 // end-of-central-directory record.
179 if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
180 eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
181 throw new SignatureException("no signature in file (bad footer)");
182 }
183
184 for (int i = 4; i < eocd.length-3; ++i) {
185 if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
186 eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
187 throw new SignatureException("EOCD marker found after start of EOCD");
188 }
189 }
190
191 // The following code is largely copied from
192 // JarUtils.verifySignature(). We could just *call* that
193 // method here if that function didn't read the entire
194 // input (ie, the whole OTA package) into memory just to
195 // compute its message digest.
196
197 BerInputStream bis = new BerInputStream(
198 new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
199 ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
200 SignedData signedData = info.getSignedData();
201 if (signedData == null) {
202 throw new IOException("signedData is null");
203 }
204 Collection encCerts = signedData.getCertificates();
205 if (encCerts.isEmpty()) {
206 throw new IOException("encCerts is empty");
207 }
208 // Take the first certificate from the signature (packages
209 // should contain only one).
210 Iterator it = encCerts.iterator();
211 X509Certificate cert = null;
212 if (it.hasNext()) {
213 cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next());
214 } else {
215 throw new SignatureException("signature contains no certificates");
216 }
217
218 List 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<Certificate> trusted = getTrustedCerts(
230 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
231
232 PublicKey signatureKey = cert.getPublicKey();
233 boolean verified = false;
234 for (Certificate 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
248 // signatures. SignApk.java always uses SHA1withRSA, no
249 // matter what the cert says to use. Ignore
250 // cert.getSigAlgName(), and instead use whatever
251 // algorithm is used by the signature (which should be
252 // SHA1withRSA).
253
Jesse Wilson66e40c32011-01-25 09:40:57 -0800254 String da = sigInfo.getDigestAlgorithm();
Doug Zongker1af33d02010-01-05 11:28:55 -0800255 String dea = sigInfo.getDigestEncryptionAlgorithm();
256 String alg = null;
257 if (da == null || dea == null) {
258 // fall back to the cert algorithm if the sig one
259 // doesn't look right.
260 alg = cert.getSigAlgName();
261 } else {
262 alg = da + "with" + dea;
263 }
264 Signature sig = Signature.getInstance(alg);
265 sig.initVerify(cert);
266
267 // The signature covers all of the OTA package except the
268 // archive comment and its 2-byte length.
269 long toRead = fileLen - commentSize - 2;
270 long soFar = 0;
271 raf.seek(0);
272 byte[] buffer = new byte[4096];
Doug Zongkercb956572010-03-11 10:27:21 -0800273 boolean interrupted = false;
Doug Zongker1af33d02010-01-05 11:28:55 -0800274 while (soFar < toRead) {
Doug Zongkercb956572010-03-11 10:27:21 -0800275 interrupted = Thread.interrupted();
276 if (interrupted) break;
Doug Zongker1af33d02010-01-05 11:28:55 -0800277 int size = buffer.length;
278 if (soFar + size > toRead) {
279 size = (int)(toRead - soFar);
280 }
281 int read = raf.read(buffer, 0, size);
282 sig.update(buffer, 0, read);
283 soFar += read;
284
285 if (listener != null) {
286 long now = System.currentTimeMillis();
287 int p = (int)(soFar * 100 / toRead);
288 if (p > lastPercent &&
289 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
290 lastPercent = p;
291 lastPublishTime = now;
292 listener.onProgress(lastPercent);
293 }
294 }
295 }
296 if (listener != null) {
297 listener.onProgress(100);
298 }
299
Doug Zongkercb956572010-03-11 10:27:21 -0800300 if (interrupted) {
301 throw new SignatureException("verification was interrupted");
302 }
303
Doug Zongker1af33d02010-01-05 11:28:55 -0800304 if (!sig.verify(sigInfo.getEncryptedDigest())) {
305 throw new SignatureException("signature digest verification failed");
306 }
307 } finally {
308 raf.close();
309 }
310 }
311
312 /**
313 * Reboots the device in order to install the given update
314 * package.
Jeff Brown64010e82010-04-08 16:52:18 -0700315 * Requires the {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800316 *
317 * @param context the Context to use
Doug Zongker4baf6412010-09-20 13:09:57 -0700318 * @param packageFile the update package to install. Must be on
319 * a partition mountable by recovery. (The set of partitions
320 * known to recovery may vary from device to device. Generally,
321 * /cache and /data are safe.)
Doug Zongker1af33d02010-01-05 11:28:55 -0800322 *
323 * @throws IOException if writing the recovery command file
324 * fails, or if the reboot itself fails.
325 */
326 public static void installPackage(Context context, File packageFile)
327 throws IOException {
328 String filename = packageFile.getCanonicalPath();
Doug Zongker1af33d02010-01-05 11:28:55 -0800329 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
Doug Zongkere33b4002012-08-23 09:52:14 -0700330 String arg = "--update_package=" + filename +
331 "\n--locale=" + Locale.getDefault().toString();
Doug Zongker1af33d02010-01-05 11:28:55 -0800332 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.
Jeff Brown64010e82010-04-08 16:52:18 -0700340 * Requires the {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800341 *
342 * @param context the Context to use
343 *
344 * @throws IOException if writing the recovery command file
345 * fails, or if the reboot itself fails.
346 */
Jason parks4ca74dc2011-03-14 15:23:31 -0500347 public static void rebootWipeUserData(Context context) throws IOException {
348 final ConditionVariable condition = new ConditionVariable();
349
350 Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
Dianne Hackborn5ac72a22012-08-29 18:32:08 -0700351 context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
352 android.Manifest.permission.MASTER_CLEAR,
Jason parks4ca74dc2011-03-14 15:23:31 -0500353 new BroadcastReceiver() {
354 @Override
355 public void onReceive(Context context, Intent intent) {
356 condition.open();
357 }
358 }, null, 0, null, null);
359
360 // Block until the ordered broadcast has completed.
361 condition.block();
362
Doug Zongkere33b4002012-08-23 09:52:14 -0700363 bootCommand(context, "--wipe_data\n--locale=" + Locale.getDefault().toString());
Doug Zongker1af33d02010-01-05 11:28:55 -0800364 }
365
366 /**
Doug Zongker33651202011-07-19 12:45:09 -0700367 * Reboot into the recovery system to wipe the /cache partition.
Doug Zongker1af33d02010-01-05 11:28:55 -0800368 * @throws IOException if something goes wrong.
Doug Zongker1af33d02010-01-05 11:28:55 -0800369 */
Doug Zongker33651202011-07-19 12:45:09 -0700370 public static void rebootWipeCache(Context context) throws IOException {
Doug Zongkere33b4002012-08-23 09:52:14 -0700371 bootCommand(context, "--wipe_cache\n--locale=" + Locale.getDefault().toString());
Doug Zongker1af33d02010-01-05 11:28:55 -0800372 }
373
374 /**
375 * Reboot into the recovery system with the supplied argument.
376 * @param arg to pass to the recovery utility.
377 * @throws IOException if something goes wrong.
378 */
379 private static void bootCommand(Context context, String arg) throws IOException {
380 RECOVERY_DIR.mkdirs(); // In case we need it
381 COMMAND_FILE.delete(); // In case it's not writable
382 LOG_FILE.delete();
383
384 FileWriter command = new FileWriter(COMMAND_FILE);
385 try {
386 command.write(arg);
387 command.write("\n");
388 } finally {
389 command.close();
390 }
391
392 // Having written the command file, go ahead and reboot
393 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
394 pm.reboot("recovery");
395
396 throw new IOException("Reboot failed (no permissions?)");
397 }
398
399 /**
400 * Called after booting to process and remove recovery-related files.
401 * @return the log file from recovery, or null if none was found.
402 *
403 * @hide
404 */
405 public static String handleAftermath() {
406 // Record the tail of the LOG_FILE
407 String log = null;
408 try {
409 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
410 } catch (FileNotFoundException e) {
411 Log.i(TAG, "No recovery log file");
412 } catch (IOException e) {
413 Log.e(TAG, "Error reading recovery log", e);
414 }
415
Doug Zongker3d5040f82011-04-12 09:23:51 -0700416 // Delete everything in RECOVERY_DIR except those beginning
417 // with LAST_PREFIX
Doug Zongker1af33d02010-01-05 11:28:55 -0800418 String[] names = RECOVERY_DIR.list();
419 for (int i = 0; names != null && i < names.length; i++) {
Doug Zongker3d5040f82011-04-12 09:23:51 -0700420 if (names[i].startsWith(LAST_PREFIX)) continue;
Doug Zongker1af33d02010-01-05 11:28:55 -0800421 File f = new File(RECOVERY_DIR, names[i]);
422 if (!f.delete()) {
423 Log.e(TAG, "Can't delete: " + f);
424 } else {
425 Log.i(TAG, "Deleted: " + f);
426 }
427 }
428
429 return log;
430 }
431
432 private void RecoverySystem() { } // Do not instantiate
433}