RecoverySystem.java revision 1af33d0ddc2f50ade146e4d48e2feb6f1d553427
11af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker/* 21af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Copyright (C) 2010 The Android Open Source Project 31af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 41af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Licensed under the Apache License, Version 2.0 (the "License"); 51af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * you may not use this file except in compliance with the License. 61af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * You may obtain a copy of the License at 71af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 81af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * http://www.apache.org/licenses/LICENSE-2.0 91af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 101af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Unless required by applicable law or agreed to in writing, software 111af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * distributed under the License is distributed on an "AS IS" BASIS, 121af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 131af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * See the License for the specific language governing permissions and 141af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * limitations under the License. 151af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 161af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 171af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerpackage android.os; 181af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 191af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.io.ByteArrayInputStream; 201af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.io.File; 211af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.io.FileNotFoundException; 221af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.io.FileWriter; 231af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.io.IOException; 241af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.io.RandomAccessFile; 251af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.security.GeneralSecurityException; 261af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.security.PublicKey; 271af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.security.Signature; 281af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.security.SignatureException; 291af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.security.cert.Certificate; 301af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.security.cert.CertificateFactory; 311af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.security.cert.X509Certificate; 321af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.util.Collection; 331af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.util.Enumeration; 341af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.util.HashSet; 351af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.util.Iterator; 361af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.util.List; 371af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.util.zip.ZipEntry; 381af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport java.util.zip.ZipFile; 391af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 401af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport android.content.Context; 411af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport android.util.Log; 421af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 431af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport org.apache.harmony.security.asn1.BerInputStream; 441af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport org.apache.harmony.security.pkcs7.ContentInfo; 451af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport org.apache.harmony.security.pkcs7.SignedData; 461af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport org.apache.harmony.security.pkcs7.SignerInfo; 471af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerimport org.apache.harmony.security.provider.cert.X509CertImpl; 481af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 491af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker/** 501af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * RecoverySystem contains methods for interacting with the Android 511af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * recovery system (the separate partition that can be used to install 521af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * system updates, wipe user data, etc.) 531af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 541af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @pending 551af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 561af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongkerpublic class RecoverySystem { 571af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static final String TAG = "RecoverySystem"; 581af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 591af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 601af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Default location of zip file containing public keys (X509 611af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * certs) authorized to sign OTA updates. 621af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 631af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static final File DEFAULT_KEYSTORE = 641af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker new File("/system/etc/security/otacerts.zip"); 651af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 661af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** Send progress to listeners no more often than this (in ms). */ 671af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; 681af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 691af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** Used to communicate with recovery. See bootable/recovery/recovery.c. */ 701af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static File RECOVERY_DIR = new File("/cache/recovery"); 711af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); 721af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static File LOG_FILE = new File(RECOVERY_DIR, "log"); 731af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 741af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // Length limits for reading files. 751af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static int LOG_FILE_MAX_LENGTH = 8 * 1024; 761af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 771af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 781af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Interface definition for a callback to be invoked regularly as 791af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * verification proceeds. 801af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 811af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker public interface ProgressListener { 821af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 831af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Called periodically as the verification progresses. 841af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 851af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param progress the approximate percentage of the 861af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * verification that has been completed, ranging from 0 871af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * to 100 (inclusive). 881af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 891af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker public void onProgress(int progress); 901af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 911af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 921af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** @return the set of certs that can be used to sign an OTA package. */ 931af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static HashSet<Certificate> getTrustedCerts(File keystore) 941af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throws IOException, GeneralSecurityException { 951af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker HashSet<Certificate> trusted = new HashSet<Certificate>(); 961af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (keystore == null) { 971af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker keystore = DEFAULT_KEYSTORE; 981af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 991af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker ZipFile zip = new ZipFile(keystore); 1001af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker try { 1011af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker CertificateFactory cf = CertificateFactory.getInstance("X.509"); 1021af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Enumeration<? extends ZipEntry> entries = zip.entries(); 1031af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker while (entries.hasMoreElements()) { 1041af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker ZipEntry entry = entries.nextElement(); 1051af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker trusted.add(cf.generateCertificate(zip.getInputStream(entry))); 1061af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1071af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } finally { 1081af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker zip.close(); 1091af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1101af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker return trusted; 1111af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1121af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1131af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 1141af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Verify the cryptographic signature of a system update package 1151af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * before installing it. Note that the package is also verified 1161af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * separately by the installer once the device is rebooted into 1171af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * the recovery system. This function will return only if the 1181af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * package was successfully verified; otherwise it will throw an 1191af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * exception. 1201af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 1211af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Verification of a package can take significant time, so this 1221af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * function should not be called from a UI thread. 1231af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 1241af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param packageFile the package to be verified 1251af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param listener an object to receive periodic progress 1261af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * updates as verification proceeds. May be null. 1271af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param deviceCertsZipFile the zip file of certificates whose 1281af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * public keys we will accept. Verification succeeds if the 1291af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * package is signed by the private key corresponding to any 1301af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * public key in this file. May be null to use the system default 1311af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * file (currently "/system/etc/security/otacerts.zip"). 1321af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 1331af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @throws IOException if there were any errors reading the 1341af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * package or certs files. 1351af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @throws GeneralSecurityException if verification failed 1361af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 1371af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker public static void verifyPackage(File packageFile, 1381af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker ProgressListener listener, 1391af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker File deviceCertsZipFile) 1401af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throws IOException, GeneralSecurityException { 1411af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker long fileLen = packageFile.length(); 1421af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1431af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); 1441af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker try { 1451af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker int lastPercent = 0; 1461af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker long lastPublishTime = System.currentTimeMillis(); 1471af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (listener != null) { 1481af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker listener.onProgress(lastPercent); 1491af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1501af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1511af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker raf.seek(fileLen - 6); 1521af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker byte[] footer = new byte[6]; 1531af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker raf.readFully(footer); 1541af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1551af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { 1561af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new SignatureException("no signature in file (no footer)"); 1571af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1581af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1591af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); 1601af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); 1611af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Log.v(TAG, String.format("comment size %d; signature start %d", 1621af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker commentSize, signatureStart)); 1631af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1641af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker byte[] eocd = new byte[commentSize + 22]; 1651af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker raf.seek(fileLen - (commentSize + 22)); 1661af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker raf.readFully(eocd); 1671af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1681af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // Check that we have found the start of the 1691af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // end-of-central-directory record. 1701af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || 1711af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { 1721af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new SignatureException("no signature in file (bad footer)"); 1731af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1741af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1751af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker for (int i = 4; i < eocd.length-3; ++i) { 1761af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && 1771af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { 1781af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new SignatureException("EOCD marker found after start of EOCD"); 1791af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1801af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1811af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1821af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // The following code is largely copied from 1831af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // JarUtils.verifySignature(). We could just *call* that 1841af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // method here if that function didn't read the entire 1851af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // input (ie, the whole OTA package) into memory just to 1861af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // compute its message digest. 1871af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 1881af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker BerInputStream bis = new BerInputStream( 1891af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); 1901af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis); 1911af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker SignedData signedData = info.getSignedData(); 1921af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (signedData == null) { 1931af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new IOException("signedData is null"); 1941af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1951af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Collection encCerts = signedData.getCertificates(); 1961af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (encCerts.isEmpty()) { 1971af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new IOException("encCerts is empty"); 1981af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 1991af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // Take the first certificate from the signature (packages 2001af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // should contain only one). 2011af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Iterator it = encCerts.iterator(); 2021af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker X509Certificate cert = null; 2031af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (it.hasNext()) { 2041af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next()); 2051af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } else { 2061af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new SignatureException("signature contains no certificates"); 2071af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2081af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2091af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker List sigInfos = signedData.getSignerInfos(); 2101af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker SignerInfo sigInfo; 2111af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (!sigInfos.isEmpty()) { 2121af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker sigInfo = (SignerInfo)sigInfos.get(0); 2131af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } else { 2141af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new IOException("no signer infos!"); 2151af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2161af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2171af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // Check that the public key of the certificate contained 2181af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // in the package equals one of our trusted public keys. 2191af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2201af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker HashSet<Certificate> trusted = getTrustedCerts( 2211af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); 2221af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2231af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker PublicKey signatureKey = cert.getPublicKey(); 2241af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker boolean verified = false; 2251af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker for (Certificate c : trusted) { 2261af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (c.getPublicKey().equals(signatureKey)) { 2271af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker verified = true; 2281af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker break; 2291af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2301af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2311af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (!verified) { 2321af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new SignatureException("signature doesn't match any trusted key"); 2331af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2341af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2351af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // The signature cert matches a trusted key. Now verify that 2361af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // the digest in the cert matches the actual file data. 2371af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2381af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // The verifier in recovery *only* handles SHA1withRSA 2391af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // signatures. SignApk.java always uses SHA1withRSA, no 2401af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // matter what the cert says to use. Ignore 2411af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // cert.getSigAlgName(), and instead use whatever 2421af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // algorithm is used by the signature (which should be 2431af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // SHA1withRSA). 2441af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2451af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker String da = sigInfo.getdigestAlgorithm(); 2461af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker String dea = sigInfo.getDigestEncryptionAlgorithm(); 2471af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker String alg = null; 2481af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (da == null || dea == null) { 2491af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // fall back to the cert algorithm if the sig one 2501af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // doesn't look right. 2511af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker alg = cert.getSigAlgName(); 2521af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } else { 2531af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker alg = da + "with" + dea; 2541af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2551af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Signature sig = Signature.getInstance(alg); 2561af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker sig.initVerify(cert); 2571af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2581af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // The signature covers all of the OTA package except the 2591af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // archive comment and its 2-byte length. 2601af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker long toRead = fileLen - commentSize - 2; 2611af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker long soFar = 0; 2621af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker raf.seek(0); 2631af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker byte[] buffer = new byte[4096]; 2641af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker while (soFar < toRead) { 2651af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker int size = buffer.length; 2661af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (soFar + size > toRead) { 2671af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker size = (int)(toRead - soFar); 2681af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2691af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker int read = raf.read(buffer, 0, size); 2701af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker sig.update(buffer, 0, read); 2711af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker soFar += read; 2721af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2731af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (listener != null) { 2741af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker long now = System.currentTimeMillis(); 2751af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker int p = (int)(soFar * 100 / toRead); 2761af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (p > lastPercent && 2771af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { 2781af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker lastPercent = p; 2791af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker lastPublishTime = now; 2801af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker listener.onProgress(lastPercent); 2811af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2821af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2831af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2841af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (listener != null) { 2851af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker listener.onProgress(100); 2861af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2871af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2881af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (!sig.verify(sigInfo.getEncryptedDigest())) { 2891af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new SignatureException("signature digest verification failed"); 2901af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2911af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } finally { 2921af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker raf.close(); 2931af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2941af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 2951af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 2961af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 2971af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Reboots the device in order to install the given update 2981af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * package. 2991af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Requires the {@link android.Manifest.permission#REBOOT} 3001af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} 3011af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * permissions. 3021af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 3031af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param context the Context to use 3041af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param packageFile the update package to install. Currently 3051af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * must be on the /cache or /data partitions. 3061af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 3071af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @throws IOException if writing the recovery command file 3081af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * fails, or if the reboot itself fails. 3091af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 3101af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker public static void installPackage(Context context, File packageFile) 3111af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throws IOException { 3121af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker String filename = packageFile.getCanonicalPath(); 3131af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3141af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (filename.startsWith("/cache/")) { 3151af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker filename = "CACHE:" + filename.substring(7); 3161af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } else if (filename.startsWith("/data/")) { 3171af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker filename = "DATA:" + filename.substring(6); 3181af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } else { 3191af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new IllegalArgumentException( 3201af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker "Must start with /cache or /data: " + filename); 3211af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 3221af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); 3231af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker String arg = "--update_package=" + filename; 3241af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker bootCommand(context, arg); 3251af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 3261af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3271af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 3281af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Reboots the device and wipes the user data partition. This is 3291af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * sometimes called a "factory reset", which is something of a 3301af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * misnomer because the system partition is not restored to its 3311af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * factory state. 3321af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Requires the {@link android.Manifest.permission#REBOOT} 3331af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} 3341af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * permissions. 3351af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 3361af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param context the Context to use 3371af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 3381af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @throws IOException if writing the recovery command file 3391af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * fails, or if the reboot itself fails. 3401af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 3411af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker public static void rebootWipeUserData(Context context) 3421af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throws IOException { 3431af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker bootCommand(context, "--wipe_data"); 3441af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 3451af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3461af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 3471af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Reboot into the recovery system to wipe the /data partition and toggle 3481af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Encrypted File Systems on/off. 3491af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. 3501af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @throws IOException if something goes wrong. 3511af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 3521af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @hide 3531af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 3541af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker public static void rebootToggleEFS(Context context, boolean efsEnabled) 3551af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throws IOException { 3561af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (efsEnabled) { 3571af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker bootCommand(context, "--set_encrypted_filesystem=on"); 3581af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } else { 3591af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker bootCommand(context, "--set_encrypted_filesystem=off"); 3601af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 3611af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 3621af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3631af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 3641af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Reboot into the recovery system with the supplied argument. 3651af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @param arg to pass to the recovery utility. 3661af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @throws IOException if something goes wrong. 3671af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 3681af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private static void bootCommand(Context context, String arg) throws IOException { 3691af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker RECOVERY_DIR.mkdirs(); // In case we need it 3701af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker COMMAND_FILE.delete(); // In case it's not writable 3711af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker LOG_FILE.delete(); 3721af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3731af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker FileWriter command = new FileWriter(COMMAND_FILE); 3741af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker try { 3751af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker command.write(arg); 3761af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker command.write("\n"); 3771af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } finally { 3781af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker command.close(); 3791af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 3801af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3811af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // Having written the command file, go ahead and reboot 3821af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 3831af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker pm.reboot("recovery"); 3841af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3851af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker throw new IOException("Reboot failed (no permissions?)"); 3861af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 3871af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 3881af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker /** 3891af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * Called after booting to process and remove recovery-related files. 3901af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @return the log file from recovery, or null if none was found. 3911af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * 3921af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker * @hide 3931af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker */ 3941af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker public static String handleAftermath() { 3951af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // Record the tail of the LOG_FILE 3961af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker String log = null; 3971af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker try { 3981af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); 3991af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } catch (FileNotFoundException e) { 4001af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Log.i(TAG, "No recovery log file"); 4011af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } catch (IOException e) { 4021af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Log.e(TAG, "Error reading recovery log", e); 4031af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 4041af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 4051af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker // Delete everything in RECOVERY_DIR 4061af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker String[] names = RECOVERY_DIR.list(); 4071af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker for (int i = 0; names != null && i < names.length; i++) { 4081af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker File f = new File(RECOVERY_DIR, names[i]); 4091af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker if (!f.delete()) { 4101af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Log.e(TAG, "Can't delete: " + f); 4111af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } else { 4121af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker Log.i(TAG, "Deleted: " + f); 4131af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 4141af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 4151af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 4161af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker return log; 4171af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker } 4181af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker 4191af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker private void RecoverySystem() { } // Do not instantiate 4201af33d0ddc2f50ade146e4d48e2feb6f1d553427Doug Zongker} 421