blob: ae605fb3ac5d1a4ab46e932323495a4bba1954ab [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;
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
Doug Zongker1af33d02010-01-05 11:28:55 -080045import 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.)
Doug Zongker1af33d02010-01-05 11:28:55 -080055 */
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");
Doug Zongker3d5040f82011-04-12 09:23:51 -070073 private static String LAST_PREFIX = "last_";
Doug Zongker1af33d02010-01-05 11:28:55 -080074
75 // Length limits for reading files.
Dan Egnorc95142d2010-03-11 12:31:23 -080076 private static int LOG_FILE_MAX_LENGTH = 64 * 1024;
Doug Zongker1af33d02010-01-05 11:28:55 -080077
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
Doug Zongkercb956572010-03-11 10:27:21 -0800123 * 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).
Doug Zongker1af33d02010-01-05 11:28:55 -0800127 *
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
Jesse Wilson66e40c32011-01-25 09:40:57 -0800249 String da = sigInfo.getDigestAlgorithm();
Doug Zongker1af33d02010-01-05 11:28:55 -0800250 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];
Doug Zongkercb956572010-03-11 10:27:21 -0800268 boolean interrupted = false;
Doug Zongker1af33d02010-01-05 11:28:55 -0800269 while (soFar < toRead) {
Doug Zongkercb956572010-03-11 10:27:21 -0800270 interrupted = Thread.interrupted();
271 if (interrupted) break;
Doug Zongker1af33d02010-01-05 11:28:55 -0800272 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
Doug Zongkercb956572010-03-11 10:27:21 -0800295 if (interrupted) {
296 throw new SignatureException("verification was interrupted");
297 }
298
Doug Zongker1af33d02010-01-05 11:28:55 -0800299 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.
Jeff Brown64010e82010-04-08 16:52:18 -0700310 * Requires the {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800311 *
312 * @param context the Context to use
Doug Zongker4baf6412010-09-20 13:09:57 -0700313 * @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.)
Doug Zongker1af33d02010-01-05 11:28:55 -0800317 *
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();
Doug Zongker1af33d02010-01-05 11:28:55 -0800324 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.
Jeff Brown64010e82010-04-08 16:52:18 -0700334 * Requires the {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800335 *
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 */
Jason parks4ca74dc2011-03-14 15:23:31 -0500341 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
Doug Zongker1af33d02010-01-05 11:28:55 -0800356 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 */
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 */
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 */
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
Doug Zongker3d5040f82011-04-12 09:23:51 -0700418 // Delete everything in RECOVERY_DIR except those beginning
419 // with LAST_PREFIX
Doug Zongker1af33d02010-01-05 11:28:55 -0800420 String[] names = RECOVERY_DIR.list();
421 for (int i = 0; names != null && i < names.length; i++) {
Doug Zongker3d5040f82011-04-12 09:23:51 -0700422 if (names[i].startsWith(LAST_PREFIX)) continue;
Doug Zongker1af33d02010-01-05 11:28:55 -0800423 File f = new File(RECOVERY_DIR, names[i]);
424 if (!f.delete()) {
425 Log.e(TAG, "Can't delete: " + f);
426 } else {
427 Log.i(TAG, "Deleted: " + f);
428 }
429 }
430
431 return log;
432 }
433
434 private void RecoverySystem() { } // Do not instantiate
435}