blob: 5e20dec3fde04a08bbeeb12946aa133a3c55435d [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
Doug Zongkerc9a9ffc2013-04-10 12:33:01 -0700247 // The verifier in recovery only handles SHA1withRSA and
248 // SHA256withRSA signatures. SignApk chooses which to use
249 // based on the signature algorithm of the cert:
250 //
251 // "SHA256withRSA" cert -> "SHA256withRSA" signature
252 // "SHA1withRSA" cert -> "SHA1withRSA" signature
253 // "MD5withRSA" cert -> "SHA1withRSA" signature (for backwards compatibility)
254 // any other cert -> SignApk fails
255 //
256 // Here we ignore whatever the cert says, and instead use
257 // whatever algorithm is used by the signature.
Doug Zongker1af33d02010-01-05 11:28:55 -0800258
Jesse Wilson66e40c32011-01-25 09:40:57 -0800259 String da = sigInfo.getDigestAlgorithm();
Doug Zongker1af33d02010-01-05 11:28:55 -0800260 String dea = sigInfo.getDigestEncryptionAlgorithm();
261 String alg = null;
262 if (da == null || dea == null) {
263 // fall back to the cert algorithm if the sig one
264 // doesn't look right.
265 alg = cert.getSigAlgName();
266 } else {
267 alg = da + "with" + dea;
268 }
269 Signature sig = Signature.getInstance(alg);
270 sig.initVerify(cert);
271
272 // The signature covers all of the OTA package except the
273 // archive comment and its 2-byte length.
274 long toRead = fileLen - commentSize - 2;
275 long soFar = 0;
276 raf.seek(0);
277 byte[] buffer = new byte[4096];
Doug Zongkercb956572010-03-11 10:27:21 -0800278 boolean interrupted = false;
Doug Zongker1af33d02010-01-05 11:28:55 -0800279 while (soFar < toRead) {
Doug Zongkercb956572010-03-11 10:27:21 -0800280 interrupted = Thread.interrupted();
281 if (interrupted) break;
Doug Zongker1af33d02010-01-05 11:28:55 -0800282 int size = buffer.length;
283 if (soFar + size > toRead) {
284 size = (int)(toRead - soFar);
285 }
286 int read = raf.read(buffer, 0, size);
287 sig.update(buffer, 0, read);
288 soFar += read;
289
290 if (listener != null) {
291 long now = System.currentTimeMillis();
292 int p = (int)(soFar * 100 / toRead);
293 if (p > lastPercent &&
294 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
295 lastPercent = p;
296 lastPublishTime = now;
297 listener.onProgress(lastPercent);
298 }
299 }
300 }
301 if (listener != null) {
302 listener.onProgress(100);
303 }
304
Doug Zongkercb956572010-03-11 10:27:21 -0800305 if (interrupted) {
306 throw new SignatureException("verification was interrupted");
307 }
308
Doug Zongker1af33d02010-01-05 11:28:55 -0800309 if (!sig.verify(sigInfo.getEncryptedDigest())) {
310 throw new SignatureException("signature digest verification failed");
311 }
312 } finally {
313 raf.close();
314 }
315 }
316
317 /**
318 * Reboots the device in order to install the given update
319 * package.
Jeff Brown64010e82010-04-08 16:52:18 -0700320 * Requires the {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800321 *
322 * @param context the Context to use
Doug Zongker4baf6412010-09-20 13:09:57 -0700323 * @param packageFile the update package to install. Must be on
324 * a partition mountable by recovery. (The set of partitions
325 * known to recovery may vary from device to device. Generally,
326 * /cache and /data are safe.)
Doug Zongker1af33d02010-01-05 11:28:55 -0800327 *
328 * @throws IOException if writing the recovery command file
329 * fails, or if the reboot itself fails.
330 */
331 public static void installPackage(Context context, File packageFile)
332 throws IOException {
333 String filename = packageFile.getCanonicalPath();
Doug Zongker1af33d02010-01-05 11:28:55 -0800334 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
Doug Zongkere33b4002012-08-23 09:52:14 -0700335 String arg = "--update_package=" + filename +
336 "\n--locale=" + Locale.getDefault().toString();
Doug Zongker1af33d02010-01-05 11:28:55 -0800337 bootCommand(context, arg);
338 }
339
340 /**
341 * Reboots the device and wipes the user data partition. This is
342 * sometimes called a "factory reset", which is something of a
343 * misnomer because the system partition is not restored to its
344 * factory state.
Jeff Brown64010e82010-04-08 16:52:18 -0700345 * Requires the {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800346 *
347 * @param context the Context to use
348 *
349 * @throws IOException if writing the recovery command file
350 * fails, or if the reboot itself fails.
351 */
Jason parks4ca74dc2011-03-14 15:23:31 -0500352 public static void rebootWipeUserData(Context context) throws IOException {
353 final ConditionVariable condition = new ConditionVariable();
354
355 Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
Dianne Hackborn5ac72a22012-08-29 18:32:08 -0700356 context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
357 android.Manifest.permission.MASTER_CLEAR,
Jason parks4ca74dc2011-03-14 15:23:31 -0500358 new BroadcastReceiver() {
359 @Override
360 public void onReceive(Context context, Intent intent) {
361 condition.open();
362 }
363 }, null, 0, null, null);
364
365 // Block until the ordered broadcast has completed.
366 condition.block();
367
Doug Zongkere33b4002012-08-23 09:52:14 -0700368 bootCommand(context, "--wipe_data\n--locale=" + Locale.getDefault().toString());
Doug Zongker1af33d02010-01-05 11:28:55 -0800369 }
370
371 /**
Doug Zongker33651202011-07-19 12:45:09 -0700372 * Reboot into the recovery system to wipe the /cache partition.
Doug Zongker1af33d02010-01-05 11:28:55 -0800373 * @throws IOException if something goes wrong.
Doug Zongker1af33d02010-01-05 11:28:55 -0800374 */
Doug Zongker33651202011-07-19 12:45:09 -0700375 public static void rebootWipeCache(Context context) throws IOException {
Doug Zongkere33b4002012-08-23 09:52:14 -0700376 bootCommand(context, "--wipe_cache\n--locale=" + Locale.getDefault().toString());
Doug Zongker1af33d02010-01-05 11:28:55 -0800377 }
378
379 /**
380 * Reboot into the recovery system with the supplied argument.
381 * @param arg to pass to the recovery utility.
382 * @throws IOException if something goes wrong.
383 */
384 private static void bootCommand(Context context, String arg) throws IOException {
385 RECOVERY_DIR.mkdirs(); // In case we need it
386 COMMAND_FILE.delete(); // In case it's not writable
387 LOG_FILE.delete();
388
389 FileWriter command = new FileWriter(COMMAND_FILE);
390 try {
391 command.write(arg);
392 command.write("\n");
393 } finally {
394 command.close();
395 }
396
397 // Having written the command file, go ahead and reboot
398 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
399 pm.reboot("recovery");
400
401 throw new IOException("Reboot failed (no permissions?)");
402 }
403
404 /**
405 * Called after booting to process and remove recovery-related files.
406 * @return the log file from recovery, or null if none was found.
407 *
408 * @hide
409 */
410 public static String handleAftermath() {
411 // Record the tail of the LOG_FILE
412 String log = null;
413 try {
414 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
415 } catch (FileNotFoundException e) {
416 Log.i(TAG, "No recovery log file");
417 } catch (IOException e) {
418 Log.e(TAG, "Error reading recovery log", e);
419 }
420
Doug Zongker3d5040f82011-04-12 09:23:51 -0700421 // Delete everything in RECOVERY_DIR except those beginning
422 // with LAST_PREFIX
Doug Zongker1af33d02010-01-05 11:28:55 -0800423 String[] names = RECOVERY_DIR.list();
424 for (int i = 0; names != null && i < names.length; i++) {
Doug Zongker3d5040f82011-04-12 09:23:51 -0700425 if (names[i].startsWith(LAST_PREFIX)) continue;
Doug Zongker1af33d02010-01-05 11:28:55 -0800426 File f = new File(RECOVERY_DIR, names[i]);
427 if (!f.delete()) {
428 Log.e(TAG, "Can't delete: " + f);
429 } else {
430 Log.i(TAG, "Deleted: " + f);
431 }
432 }
433
434 return log;
435 }
436
437 private void RecoverySystem() { } // Do not instantiate
438}