blob: b879c83eeadcb6ddf676a1223cf8fc13adf6b368 [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;
Julia Reynoldsfe053802014-06-30 11:41:32 -040022import android.os.UserManager;
Jeff Sharkey004a4b22014-09-24 11:45:24 -070023import android.text.TextUtils;
Jason parks4ca74dc2011-03-14 15:23:31 -050024import android.util.Log;
25
Doug Zongker1af33d02010-01-05 11:28:55 -080026import java.io.ByteArrayInputStream;
27import java.io.File;
28import java.io.FileNotFoundException;
29import java.io.FileWriter;
30import java.io.IOException;
Doug Zongkere2d58e92011-11-04 13:39:47 -070031import java.io.InputStream;
Doug Zongker1af33d02010-01-05 11:28:55 -080032import java.io.RandomAccessFile;
33import java.security.GeneralSecurityException;
34import java.security.PublicKey;
35import java.security.Signature;
36import java.security.SignatureException;
Doug Zongker1af33d02010-01-05 11:28:55 -080037import java.security.cert.CertificateFactory;
38import java.security.cert.X509Certificate;
Doug Zongker1af33d02010-01-05 11:28:55 -080039import 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;
Kenny Root27e54942013-10-14 11:17:25 -070051import org.apache.harmony.security.x509.Certificate;
Doug Zongker1af33d02010-01-05 11:28:55 -080052
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. */
Kenny Root27e54942013-10-14 11:17:25 -070096 private static HashSet<X509Certificate> getTrustedCerts(File keystore)
Doug Zongker1af33d02010-01-05 11:28:55 -080097 throws IOException, GeneralSecurityException {
Kenny Root27e54942013-10-14 11:17:25 -070098 HashSet<X509Certificate> trusted = new HashSet<X509Certificate>();
Doug Zongker1af33d02010-01-05 11:28:55 -080099 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 {
Kenny Root27e54942013-10-14 11:17:25 -0700110 trusted.add((X509Certificate) cf.generateCertificate(is));
Doug Zongkere2d58e92011-11-04 13:39:47 -0700111 } 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 }
Kenny Root27e54942013-10-14 11:17:25 -0700204 List<Certificate> encCerts = signedData.getCertificates();
Doug Zongker1af33d02010-01-05 11:28:55 -0800205 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).
Kenny Root27e54942013-10-14 11:17:25 -0700210 Iterator<Certificate> it = encCerts.iterator();
Doug Zongker1af33d02010-01-05 11:28:55 -0800211 X509Certificate cert = null;
212 if (it.hasNext()) {
Kenny Root27e54942013-10-14 11:17:25 -0700213 CertificateFactory cf = CertificateFactory.getInstance("X.509");
214 InputStream is = new ByteArrayInputStream(it.next().getEncoded());
215 cert = (X509Certificate) cf.generateCertificate(is);
Doug Zongker1af33d02010-01-05 11:28:55 -0800216 } else {
217 throw new SignatureException("signature contains no certificates");
218 }
219
Kenny Root27e54942013-10-14 11:17:25 -0700220 List<SignerInfo> sigInfos = signedData.getSignerInfos();
Doug Zongker1af33d02010-01-05 11:28:55 -0800221 SignerInfo sigInfo;
222 if (!sigInfos.isEmpty()) {
223 sigInfo = (SignerInfo)sigInfos.get(0);
224 } else {
225 throw new IOException("no signer infos!");
226 }
227
228 // Check that the public key of the certificate contained
229 // in the package equals one of our trusted public keys.
230
Kenny Root27e54942013-10-14 11:17:25 -0700231 HashSet<X509Certificate> trusted = getTrustedCerts(
Doug Zongker1af33d02010-01-05 11:28:55 -0800232 deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);
233
234 PublicKey signatureKey = cert.getPublicKey();
235 boolean verified = false;
Kenny Root27e54942013-10-14 11:17:25 -0700236 for (X509Certificate c : trusted) {
Doug Zongker1af33d02010-01-05 11:28:55 -0800237 if (c.getPublicKey().equals(signatureKey)) {
238 verified = true;
239 break;
240 }
241 }
242 if (!verified) {
243 throw new SignatureException("signature doesn't match any trusted key");
244 }
245
246 // The signature cert matches a trusted key. Now verify that
247 // the digest in the cert matches the actual file data.
248
Doug Zongkerc9a9ffc2013-04-10 12:33:01 -0700249 // The verifier in recovery only handles SHA1withRSA and
250 // SHA256withRSA signatures. SignApk chooses which to use
251 // based on the signature algorithm of the cert:
252 //
253 // "SHA256withRSA" cert -> "SHA256withRSA" signature
254 // "SHA1withRSA" cert -> "SHA1withRSA" signature
255 // "MD5withRSA" cert -> "SHA1withRSA" signature (for backwards compatibility)
256 // any other cert -> SignApk fails
257 //
258 // Here we ignore whatever the cert says, and instead use
259 // whatever algorithm is used by the signature.
Doug Zongker1af33d02010-01-05 11:28:55 -0800260
Jesse Wilson66e40c32011-01-25 09:40:57 -0800261 String da = sigInfo.getDigestAlgorithm();
Doug Zongker1af33d02010-01-05 11:28:55 -0800262 String dea = sigInfo.getDigestEncryptionAlgorithm();
263 String alg = null;
264 if (da == null || dea == null) {
265 // fall back to the cert algorithm if the sig one
266 // doesn't look right.
267 alg = cert.getSigAlgName();
268 } else {
269 alg = da + "with" + dea;
270 }
271 Signature sig = Signature.getInstance(alg);
272 sig.initVerify(cert);
273
274 // The signature covers all of the OTA package except the
275 // archive comment and its 2-byte length.
276 long toRead = fileLen - commentSize - 2;
277 long soFar = 0;
278 raf.seek(0);
279 byte[] buffer = new byte[4096];
Doug Zongkercb956572010-03-11 10:27:21 -0800280 boolean interrupted = false;
Doug Zongker1af33d02010-01-05 11:28:55 -0800281 while (soFar < toRead) {
Doug Zongkercb956572010-03-11 10:27:21 -0800282 interrupted = Thread.interrupted();
283 if (interrupted) break;
Doug Zongker1af33d02010-01-05 11:28:55 -0800284 int size = buffer.length;
285 if (soFar + size > toRead) {
286 size = (int)(toRead - soFar);
287 }
288 int read = raf.read(buffer, 0, size);
289 sig.update(buffer, 0, read);
290 soFar += read;
291
292 if (listener != null) {
293 long now = System.currentTimeMillis();
294 int p = (int)(soFar * 100 / toRead);
295 if (p > lastPercent &&
296 now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
297 lastPercent = p;
298 lastPublishTime = now;
299 listener.onProgress(lastPercent);
300 }
301 }
302 }
303 if (listener != null) {
304 listener.onProgress(100);
305 }
306
Doug Zongkercb956572010-03-11 10:27:21 -0800307 if (interrupted) {
308 throw new SignatureException("verification was interrupted");
309 }
310
Doug Zongker1af33d02010-01-05 11:28:55 -0800311 if (!sig.verify(sigInfo.getEncryptedDigest())) {
312 throw new SignatureException("signature digest verification failed");
313 }
314 } finally {
315 raf.close();
316 }
317 }
318
319 /**
320 * Reboots the device in order to install the given update
321 * package.
Jeff Brown64010e82010-04-08 16:52:18 -0700322 * Requires the {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800323 *
324 * @param context the Context to use
Doug Zongker4baf6412010-09-20 13:09:57 -0700325 * @param packageFile the update package to install. Must be on
326 * a partition mountable by recovery. (The set of partitions
327 * known to recovery may vary from device to device. Generally,
328 * /cache and /data are safe.)
Doug Zongker1af33d02010-01-05 11:28:55 -0800329 *
330 * @throws IOException if writing the recovery command file
331 * fails, or if the reboot itself fails.
332 */
333 public static void installPackage(Context context, File packageFile)
334 throws IOException {
335 String filename = packageFile.getCanonicalPath();
Doug Zongker1af33d02010-01-05 11:28:55 -0800336 Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700337
338 final String filenameArg = "--update_package=" + filename;
339 final String localeArg = "--locale=" + Locale.getDefault().toString();
340 bootCommand(context, filenameArg, localeArg);
Doug Zongker1af33d02010-01-05 11:28:55 -0800341 }
342
343 /**
Doug Zongkercdf00882014-03-18 12:52:04 -0700344 * Reboots the device and wipes the user data and cache
345 * partitions. This is sometimes called a "factory reset", which
346 * is something of a misnomer because the system partition is not
347 * restored to its factory state. Requires the
348 * {@link android.Manifest.permission#REBOOT} permission.
Doug Zongker1af33d02010-01-05 11:28:55 -0800349 *
350 * @param context the Context to use
351 *
352 * @throws IOException if writing the recovery command file
353 * fails, or if the reboot itself fails.
Julia Reynoldsfe053802014-06-30 11:41:32 -0400354 * @throws SecurityException if the current user is not allowed to wipe data.
Doug Zongker1af33d02010-01-05 11:28:55 -0800355 */
Jason parks4ca74dc2011-03-14 15:23:31 -0500356 public static void rebootWipeUserData(Context context) throws IOException {
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700357 rebootWipeUserData(context, false, context.getPackageName());
358 }
359
360 /** {@hide} */
361 public static void rebootWipeUserData(Context context, String reason) throws IOException {
362 rebootWipeUserData(context, false, reason);
363 }
364
365 /** {@hide} */
366 public static void rebootWipeUserData(Context context, boolean shutdown)
367 throws IOException {
368 rebootWipeUserData(context, shutdown, context.getPackageName());
Doug Zongkercdf00882014-03-18 12:52:04 -0700369 }
370
371 /**
372 * Reboots the device and wipes the user data and cache
373 * partitions. This is sometimes called a "factory reset", which
374 * is something of a misnomer because the system partition is not
375 * restored to its factory state. Requires the
376 * {@link android.Manifest.permission#REBOOT} permission.
377 *
378 * @param context the Context to use
379 * @param shutdown if true, the device will be powered down after
380 * the wipe completes, rather than being rebooted
381 * back to the regular system.
382 *
383 * @throws IOException if writing the recovery command file
384 * fails, or if the reboot itself fails.
Julia Reynoldsfe053802014-06-30 11:41:32 -0400385 * @throws SecurityException if the current user is not allowed to wipe data.
Doug Zongkercdf00882014-03-18 12:52:04 -0700386 *
387 * @hide
388 */
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700389 public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
390 throws IOException {
Julia Reynoldsfe053802014-06-30 11:41:32 -0400391 UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
392 if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
393 throw new SecurityException("Wiping data is not allowed for this user.");
394 }
Jason parks4ca74dc2011-03-14 15:23:31 -0500395 final ConditionVariable condition = new ConditionVariable();
396
397 Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
Christopher Tatee27ae552014-04-24 15:13:13 -0700398 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
Dianne Hackborn5ac72a22012-08-29 18:32:08 -0700399 context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
400 android.Manifest.permission.MASTER_CLEAR,
Jason parks4ca74dc2011-03-14 15:23:31 -0500401 new BroadcastReceiver() {
402 @Override
403 public void onReceive(Context context, Intent intent) {
404 condition.open();
405 }
406 }, null, 0, null, null);
407
408 // Block until the ordered broadcast has completed.
409 condition.block();
410
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700411 String shutdownArg = null;
Doug Zongkercdf00882014-03-18 12:52:04 -0700412 if (shutdown) {
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700413 shutdownArg = "--shutdown_after";
Doug Zongkercdf00882014-03-18 12:52:04 -0700414 }
415
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700416 String reasonArg = null;
417 if (!TextUtils.isEmpty(reason)) {
418 reasonArg = "--reason=" + sanitizeArg(reason);
419 }
420
421 final String localeArg = "--locale=" + Locale.getDefault().toString();
422 bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
Doug Zongker1af33d02010-01-05 11:28:55 -0800423 }
424
425 /**
Doug Zongker33651202011-07-19 12:45:09 -0700426 * Reboot into the recovery system to wipe the /cache partition.
Doug Zongker1af33d02010-01-05 11:28:55 -0800427 * @throws IOException if something goes wrong.
Doug Zongker1af33d02010-01-05 11:28:55 -0800428 */
Doug Zongker33651202011-07-19 12:45:09 -0700429 public static void rebootWipeCache(Context context) throws IOException {
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700430 rebootWipeCache(context, context.getPackageName());
431 }
432
433 /** {@hide} */
434 public static void rebootWipeCache(Context context, String reason) throws IOException {
435 String reasonArg = null;
436 if (!TextUtils.isEmpty(reason)) {
437 reasonArg = "--reason=" + sanitizeArg(reason);
438 }
439
440 final String localeArg = "--locale=" + Locale.getDefault().toString();
441 bootCommand(context, "--wipe_cache", reasonArg, localeArg);
Doug Zongker1af33d02010-01-05 11:28:55 -0800442 }
443
444 /**
445 * Reboot into the recovery system with the supplied argument.
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700446 * @param args to pass to the recovery utility.
Doug Zongker1af33d02010-01-05 11:28:55 -0800447 * @throws IOException if something goes wrong.
448 */
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700449 private static void bootCommand(Context context, String... args) throws IOException {
Doug Zongker1af33d02010-01-05 11:28:55 -0800450 RECOVERY_DIR.mkdirs(); // In case we need it
451 COMMAND_FILE.delete(); // In case it's not writable
452 LOG_FILE.delete();
453
454 FileWriter command = new FileWriter(COMMAND_FILE);
455 try {
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700456 for (String arg : args) {
457 if (!TextUtils.isEmpty(arg)) {
458 command.write(arg);
459 command.write("\n");
460 }
461 }
Doug Zongker1af33d02010-01-05 11:28:55 -0800462 } finally {
463 command.close();
464 }
465
466 // Having written the command file, go ahead and reboot
467 PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
Doug Zongker183415e2014-08-12 10:18:40 -0700468 pm.reboot(PowerManager.REBOOT_RECOVERY);
Doug Zongker1af33d02010-01-05 11:28:55 -0800469
470 throw new IOException("Reboot failed (no permissions?)");
471 }
472
473 /**
474 * Called after booting to process and remove recovery-related files.
475 * @return the log file from recovery, or null if none was found.
476 *
477 * @hide
478 */
479 public static String handleAftermath() {
480 // Record the tail of the LOG_FILE
481 String log = null;
482 try {
483 log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n");
484 } catch (FileNotFoundException e) {
485 Log.i(TAG, "No recovery log file");
486 } catch (IOException e) {
487 Log.e(TAG, "Error reading recovery log", e);
488 }
489
Doug Zongker3d5040f82011-04-12 09:23:51 -0700490 // Delete everything in RECOVERY_DIR except those beginning
491 // with LAST_PREFIX
Doug Zongker1af33d02010-01-05 11:28:55 -0800492 String[] names = RECOVERY_DIR.list();
493 for (int i = 0; names != null && i < names.length; i++) {
Doug Zongker3d5040f82011-04-12 09:23:51 -0700494 if (names[i].startsWith(LAST_PREFIX)) continue;
Doug Zongker1af33d02010-01-05 11:28:55 -0800495 File f = new File(RECOVERY_DIR, names[i]);
496 if (!f.delete()) {
497 Log.e(TAG, "Can't delete: " + f);
498 } else {
499 Log.i(TAG, "Deleted: " + f);
500 }
501 }
502
503 return log;
504 }
505
Jeff Sharkey004a4b22014-09-24 11:45:24 -0700506 /**
507 * Internally, recovery treats each line of the command file as a separate
508 * argv, so we only need to protect against newlines and nulls.
509 */
510 private static String sanitizeArg(String arg) {
511 arg = arg.replace('\0', '?');
512 arg = arg.replace('\n', '?');
513 return arg;
514 }
515
Andreas Gampe8c80efe2015-03-16 17:51:01 +0000516 private void RecoverySystem() { } // Do not instantiate
Doug Zongker1af33d02010-01-05 11:28:55 -0800517}