Add "secure mode" and its components (enabled by default)

Support is added to inject a public key (from config.xml) into TWRP
against which the flashable ZIP is verified. The public key is provided
through /cache and thus deemed secure from non-system/root app
manipulation. In contrast to the flashable ZIPs themselves which are
located on the internal storage, where every app can manipulate the
files. Even though the MD5 of the files is provided by the delta JSON
and verified before rebooting to recovery, there is a tiny exploitable
window there for a malicious app to manipulate or switch out ZIP files
and have its own content flashed.

If (reconstructed) ZIP signing is enabled, and injecting the public key
is enabled, "secure mode" becomes available (with user override). In
addition to verifying the update's cryptographic signature in recovery,
this mode disables the flashing of additional ZIPs from the
FlashAfterUpdate subfolder (as their origin cannot be verified in
recovery and any app can manipulate these files), and also fully
disables what little CWM compatibility there was (as we cannot do the
verification check through CWM)

Various warnings and popups have also been added to inform the user of
each situation as it occurs.
This commit is contained in:
Jorrit Jongma 2013-12-19 01:07:41 +01:00
parent e2790fdc59
commit ac09391b5b
7 changed files with 297 additions and 59 deletions

View File

@ -39,6 +39,9 @@ fail anyway (if enabled). So to save a bit of processing, this feature is
turned off by default. The needed files are generated and the client knows how
to deal with them, so enabling this feature is just a configuration switch away.
**TODO** Update this signature documentation and "secure mode". Signature
verification can now be enabled.
The produced delta files are pushed to the public download server, and the
current build is saved to a private location to serve as input for the next
differential run.

View File

@ -6,9 +6,16 @@
android:showAsAction="never"
android:title="@string/action_networks" />
<item
android:id="@+id/action_secure_mode"
android:orderInCategory="101"
android:showAsAction="never"
android:checkable="true"
android:title="@string/action_secure_mode" />
<item
android:id="@+id/action_about"
android:orderInCategory="101"
android:orderInCategory="102"
android:showAsAction="never"
android:title="@string/action_about" />

View File

@ -25,5 +25,17 @@
<string name="url_base_full">http://dl.omnirom.org/%s/</string>
<!-- Applies whole-file signature delta. Adds one extra delta step. Required if recovery verifies signatures -->
<item type="bool" name="apply_signature">false</item>
<item type="bool" name="apply_signature">true</item>
<!-- (TWRP) Set this to false if the keys below aren't your ROM's -->
<item type="bool" name="inject_signature_enable">true</item>
<!-- (TWRP) Verification signatures to inject. Produced by 'dumpkey.jar' (out/host) of the platform.x509 key used to sign the ZIP file -->
<string name="inject_signature_keys"><![CDATA[v2 {64,0x9d3ef4e7,{3451855145,2574857780,2212470067,2065828617,2220798544,1453138002,3035953543,349537325,3471576065,3709424322,1499657722,626083680,3508502098,135982109,2406850010,2674691998,3903782739,3673009508,3196976129,124737966,3727608735,3698514242,2926317182,2598715876,2200551045,3324466456,2027872794,1316834497,3538558575,4094723182,3091112109,152419065,961268200,2817719766,2542630774,735678394,2025086356,3319743251,3482513753,3754037486,2186326636,2162920719,1933319201,1362420666,3093979107,3944963833,3173846995,3307766644,4239176696,3380551792,3189093155,3679104199,4159403556,3373361362,737822358,2043192588,3446724037,2184123451,3680508975,492248740,1654088879,3739912969,188663922,4074712169},{2400585854,3884144496,312737665,3547448515,3596760612,2953776441,190371072,1980790627,3681130262,827104214,1597200957,1333455720,1391423898,4233042842,3284284880,50168935,2424437529,2771213818,3715896496,3320142743,3649069246,2702994054,839870558,1257344415,3116165843,4195920375,2497396347,1334871168,3550010104,64795091,3042249326,4155098628,123980023,3500559217,1825888674,443352554,3891428201,2484397377,4136956616,4201065713,2547196505,3411971111,2135688607,393830937,4198844531,3826748593,3979050977,1220766766,3592470842,2278136,1841247501,3507376964,3313320668,3849023694,2185649624,3043141327,1601153541,939583339,2083130022,3508853409,2068728550,3713282728,2402412627,1764295415}}]]></string>
<!-- (TWRP) Add secure mode setting. Requires 'apply_signature' and 'inject_signature_enable'. Limits flashing to TWRP only, verifies ZIP signature in TWRP, disables auto-flashing of additional ZIPs -->
<item type="bool" name="secure_mode_enable">true</item>
<!-- (TWRP) Requires 'secure_mode_enable'. Decides whether the default setting for secure mode is enabled (true) or disabled (false) -->
<item type="bool" name="secure_mode_default">true</item>
</resources>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_secure_mode">Secure</string>
<string name="secure_mode_enabled_title">Secure mode: enabled</string>
<string name="secure_mode_enabled_description"><![CDATA[The update\'s cryptographic signature will be verified before flashing to confirm its origin, and no additional updates will be flashed. This mode is only compatible with <b>TWRP</b>.]]></string>
<string name="secure_mode_disabled_title">Secure mode: disabled</string>
<string name="secure_mode_disabled_description"><![CDATA[Additional updates are flashed if present. Note that any (malicious) software package can place these updates and gain full system access. Caution is advised.]]></string>
<string name="recovery_notice_title">Notice</string>
<string name="recovery_notice_description_secure"><![CDATA[Flashing updates in the current mode is only supported on <b>TWRP</b> recoveries. Please make sure you are running a compatible recovery before continuing.]]></string>
<string name="recovery_notice_description_not_secure"><![CDATA[Flashing updates in the current mode is only officially supported on <b>TWRP</b> recoveries. <i>Official</i> <b>CWM</b> builds are <i>not</i> supported, though some <i>community-built</i> versions of <b>CWM</b> <i>may</i> still work. Please make sure you are running a compatible recovery before continuing.]]></string>
<string name="flash_after_update_notice_title">Notice</string>
<string name="flash_after_update_notice_description"><![CDATA[Additional updates are present in the <b>FlashAfterUpdate</b> folder, but secure mode is enabled. If you continue, these additional updates will <b>not</b> be flashed.]]></string>
</resources>

View File

@ -22,11 +22,16 @@
package eu.chainfire.opendelta;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Environment;
import android.preference.PreferenceManager;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
public class Config {
@ -38,16 +43,26 @@ public class Config {
}
return instance;
}
private final static String PREF_SECURE_MODE_NAME = "secure_mode";
private final static String PREF_SHOWN_RECOVERY_WARNING_SECURE_NAME = "shown_recovery_warning_secure";
private final static String PREF_SHOWN_RECOVERY_WARNING_NOT_SECURE_NAME = "shown_recovery_warning_not_secure";
private final SharedPreferences prefs;
private String property_version;
private String property_device;
private String filename_base;
private String path_base;
private String path_flash_after_update;
private String url_base_delta;
private String url_base_update;
private String url_base_full;
private boolean apply_signature;
private final String property_version;
private final String property_device;
private final String filename_base;
private final String path_base;
private final String path_flash_after_update;
private final String url_base_delta;
private final String url_base_update;
private final String url_base_full;
private final boolean apply_signature;
private final boolean inject_signature_enable;
private final String inject_signature_keys;
private final boolean secure_mode_enable;
private final boolean secure_mode_default;
/*
* Using reflection voodoo instead calling the hidden class directly, to
@ -72,6 +87,8 @@ public class Config {
}
private Config(Context context) {
prefs = PreferenceManager.getDefaultSharedPreferences(context);
Resources res = context.getResources();
property_version = getProperty(context, res.getString(R.string.property_version), "");
@ -94,6 +111,10 @@ public class Config {
url_base_full = String.format(Locale.ENGLISH, res.getString(R.string.url_base_full),
property_device);
apply_signature = res.getBoolean(R.bool.apply_signature);
inject_signature_enable = res.getBoolean(R.bool.inject_signature_enable);
inject_signature_keys = res.getString(R.string.inject_signature_keys);
secure_mode_enable = res.getBoolean(R.bool.secure_mode_enable);
secure_mode_default = res.getBoolean(R.bool.secure_mode_default);
Logger.d("property_version: %s", property_version);
Logger.d("property_device: %s", property_device);
@ -104,6 +125,10 @@ public class Config {
Logger.d("url_base_update: %s", url_base_update);
Logger.d("url_base_full: %s", url_base_full);
Logger.d("apply_signature: %d", apply_signature ? 1 : 0);
Logger.d("inject_signature_enable: %d", inject_signature_enable ? 1 : 0);
Logger.d("inject_signature_keys: %s", inject_signature_keys);
Logger.d("secure_mode_enable: %d", secure_mode_enable ? 1 : 0);
Logger.d("secure_mode_default: %d", secure_mode_default ? 1 : 0);
}
public String getFilenameBase() {
@ -133,4 +158,64 @@ public class Config {
public boolean getApplySignature() {
return apply_signature;
}
}
public boolean getInjectSignatureEnable() {
return inject_signature_enable;
}
public String getInjectSignatureKeys() {
return inject_signature_keys;
}
public boolean getSecureModeEnable() {
return apply_signature && inject_signature_enable && secure_mode_enable;
}
public boolean getSecureModeDefault() {
return secure_mode_default && getSecureModeEnable();
}
public boolean getSecureModeCurrent() {
return getSecureModeEnable() && prefs.getBoolean(PREF_SECURE_MODE_NAME, getSecureModeDefault());
}
public boolean setSecureModeCurrent(boolean enable) {
prefs.edit().putBoolean(PREF_SECURE_MODE_NAME, getSecureModeEnable() && enable).commit();
return getSecureModeCurrent();
}
public List<String> getFlashAfterUpdateZIPs() {
List<String> extras = new ArrayList<String>();
File[] files = (new File(getPathFlashAfterUpdate())).listFiles();
if (files != null) {
for (File f : files) {
if (f.getName().toLowerCase(Locale.ENGLISH).endsWith(".zip")) {
String filename = f.getAbsolutePath();
if (filename.startsWith(getPathBase())) {
extras.add(filename);
}
}
}
Collections.sort(extras);
}
return extras;
}
public boolean getShownRecoveryWarningSecure() {
return prefs.getBoolean(PREF_SHOWN_RECOVERY_WARNING_SECURE_NAME, false);
}
public void setShownRecoveryWarningSecure() {
prefs.edit().putBoolean(PREF_SHOWN_RECOVERY_WARNING_SECURE_NAME, true).commit();
}
public boolean getShownRecoveryWarningNotSecure() {
return prefs.getBoolean(PREF_SHOWN_RECOVERY_WARNING_NOT_SECURE_NAME, false);
}
public void setShownRecoveryWarningNotSecure() {
prefs.edit().putBoolean(PREF_SHOWN_RECOVERY_WARNING_NOT_SECURE_NAME, true).commit();
}
}

View File

@ -53,6 +53,8 @@ public class MainActivity extends Activity {
private ProgressBar progress = null;
private Button checkNow = null;
private Button flashNow = null;
private Config config;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -76,11 +78,20 @@ public class MainActivity extends Activity {
progress = (ProgressBar) findViewById(R.id.progress);
checkNow = (Button) findViewById(R.id.button_check_now);
flashNow = (Button) findViewById(R.id.button_flash_now);
config = Config.getInstance(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
if (!config.getSecureModeEnable()) {
menu.findItem(R.id.action_secure_mode).setVisible(false);
} else {
menu.findItem(R.id.action_secure_mode).setChecked(config.getSecureModeCurrent());
}
return true;
}
@ -170,6 +181,17 @@ public class MainActivity extends Activity {
return true;
case R.id.action_networks:
showNetworks();
return true;
case R.id.action_secure_mode:
item.setChecked(config.setSecureModeCurrent(!item.isChecked()));
(new AlertDialog.Builder(this)).
setTitle(item.isChecked() ? R.string.secure_mode_enabled_title : R.string.secure_mode_disabled_title).
setMessage(Html.fromHtml(getString(item.isChecked() ? R.string.secure_mode_enabled_description : R.string.secure_mode_disabled_description))).
setCancelable(true).
setNeutralButton(android.R.string.ok, null).
show();
return true;
case R.id.action_about:
showAbout();
@ -311,8 +333,78 @@ public class MainActivity extends Activity {
}
public void onButtonFlashNowClick(View v) {
UpdateService.startFlash(this);
checkNow.setEnabled(false);
flashNow.setEnabled(false);
flashRecoveryWarning.run();
}
private Runnable flashRecoveryWarning = new Runnable() {
@Override
public void run() {
// Show a warning message about recoveries we support, depending
// on the state of secure mode and if we've shown the message before
final Runnable next = flashWarningFlashAfterUpdateZIPs;
CharSequence message = null;
if (!config.getSecureModeCurrent() && !config.getShownRecoveryWarningNotSecure()) {
message = Html.fromHtml(getString(R.string.recovery_notice_description_not_secure));
config.setShownRecoveryWarningNotSecure();
} else if (config.getSecureModeCurrent() && !config.getShownRecoveryWarningSecure()) {
message = Html.fromHtml(getString(R.string.recovery_notice_description_secure));
config.setShownRecoveryWarningSecure();
}
if (message != null) {
(new AlertDialog.Builder(MainActivity.this)).
setTitle(R.string.recovery_notice_title).
setMessage(message).
setCancelable(true).
setNegativeButton(android.R.string.cancel, null).
setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
next.run();
}
}).
show();
} else {
next.run();
}
}
};
private Runnable flashWarningFlashAfterUpdateZIPs = new Runnable() {
@Override
public void run() {
// If we're in secure mode, but additional ZIPs to flash have been
// detected, warn the user that these will not be flashed
final Runnable next = flashStart;
if (config.getSecureModeCurrent() && (config.getFlashAfterUpdateZIPs().size() > 0)) {
(new AlertDialog.Builder(MainActivity.this)).
setTitle(R.string.flash_after_update_notice_title).
setMessage(Html.fromHtml(getString(R.string.flash_after_update_notice_description))).
setCancelable(true).
setNegativeButton(android.R.string.cancel, null).
setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
next.run();
}
}).
show();
} else {
next.run();
}
}
};
private Runnable flashStart = new Runnable() {
@Override
public void run() {
checkNow.setEnabled(false);
flashNow.setEnabled(false);
UpdateService.startFlash(MainActivity.this);
}
};
}

View File

@ -58,13 +58,15 @@ import org.json.JSONException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -102,7 +104,7 @@ public class UpdateService
intent.putExtra(EXTRA_ALARM_ID, id);
return PendingIntent.getService(context, id, intent, 0);
}
public static final String ACTION_SYSTEM_UPDATE_SETTINGS = "android.settings.SYSTEM_UPDATE_SETTINGS";
public static final String PERMISSION_ACCESS_CACHE_FILESYSTEM = "android.permission.ACCESS_CACHE_FILESYSTEM";
public static final String PERMISSION_REBOOT = "android.permission.REBOOT";
@ -924,6 +926,10 @@ public class UpdateService
return true;
}
private void writeString(OutputStream os, String s) throws UnsupportedEncodingException, IOException {
os.write((s + "\n").getBytes("UTF-8"));
}
@SuppressLint("SdCardPath")
private void flashUpdate() {
if (getPackageManager().checkPermission(PERMISSION_ACCESS_CACHE_FILESYSTEM,
@ -948,46 +954,62 @@ public class UpdateService
String path_sd = Environment.getExternalStorageDirectory() + File.separator;
flashFilename = flashFilename.substring(path_sd.length());
// Find additional ZIPs to flash
List<String> extras = new ArrayList<String>();
{
File[] files = (new File(config.getPathFlashAfterUpdate())).listFiles();
if (files != null) {
for (File f : files) {
if (f.getName().toLowerCase(Locale.ENGLISH).endsWith(".zip")) {
String filename = f.getAbsolutePath();
if (filename.startsWith(config.getPathBase())) {
extras.add(filename.substring(path_sd.length()));
}
}
}
}
Collections.sort(extras);
// Find additional ZIPs to flash, strip path to sd
List<String> extras = config.getFlashAfterUpdateZIPs();
for (int i = 0; i < extras.size(); i++) {
extras.set(i, extras.get(i).substring(path_sd.length()));
}
try {
// We're using TWRP's openrecoveryscript as primary, and CWM's
// extendedcommand as fallback. Using AOSP's command would
// break older TWRPs. extendedcommand is broken on 'official'
// CWM builds, though.
// TWRP - OpenRecoveryScript - the recovery will find the correct
// storage root for the ZIPs,
// life is nice and easy.
if ((flashFilename != null) && (!flashFilename.equals(""))) {
// storage root for the ZIPs, life is nice and easy.
//
// Optionally, we're injecting our own signature verification keys
// and verifying against those. We place these keys in /cache
// where only privileged apps can edit, contrary to the storage
// location of the ZIP itself - anyone can modify the ZIP.
// As such, flashing the ZIP without checking the whole-file
// signature coming from a secure location would be a security
// risk.
{
if (config.getInjectSignatureEnable()) {
FileOutputStream os = new FileOutputStream("/cache/recovery/keys", false);
try {
writeString(os, config.getInjectSignatureKeys());
} finally {
os.close();
}
setPermissions("/cache/recovery/keys", 0644, Process.myUid(), 2001 /* AID_CACHE */);
}
FileOutputStream os = new FileOutputStream("/cache/recovery/openrecoveryscript",
false);
try {
os.write(String.format("install %s\n", flashFilename).getBytes("UTF-8"));
for (String file : extras) {
os.write(String.format("install %s\n", file).getBytes("UTF-8"));
if (config.getInjectSignatureEnable()) {
writeString(os, "cmd cat /res/keys > /res/keys_org");
writeString(os, "cmd cat /cache/recovery/keys > /res/keys");
writeString(os, "set tw_signed_zip_verify 1");
writeString(os, String.format("install %s", flashFilename));
writeString(os, "set tw_signed_zip_verify 0");
writeString(os, "cmd cat /res/keys_org > /res/keys");
writeString(os, "cmd rm /res/keys_org");
} else {
writeString(os, String.format("install %s", flashFilename));
}
os.write(("wipe cache\n").getBytes("UTF-8"));
if (!config.getSecureModeCurrent()) {
// any program could have placed these ZIPs, so ignore them in secure mode
for (String file : extras) {
writeString(os, String.format("install %s", file));
}
}
writeString(os, "wipe cache");
} finally {
os.close();
}
setPermissions("/cache/recovery/openrecoveryscript", 0644, Process.myUid(), 2001 /* AID_CACHE */);
}
setPermissions("/cache/recovery/openrecoveryscript", 0644, Process.myUid(), 2001 /* AID_CACHE */);
// CWM - ExtendedCommand - provide paths to both internal and
// external storage locations, it's nigh impossible to know in
@ -997,31 +1019,31 @@ public class UpdateService
// versions to have them reversed. It'll give some horrible looking
// results, but it seems to continue installing even if one ZIP
// fails and produce the wanted result. Better than nothing ...
if ((flashFilename != null) && (!flashFilename.equals(""))) {
//
// We don't generate a CWM script in secure mode, because it
// doesn't support checking our custom signatures
if (!config.getSecureModeCurrent()) {
FileOutputStream os = new FileOutputStream("/cache/recovery/extendedcommand", false);
try {
os.write(String.format("install_zip(\"%s%s\");\n", "/sdcard/", flashFilename)
.getBytes("UTF-8"));
os.write(String.format("install_zip(\"%s%s\");\n", "/emmc/", flashFilename)
.getBytes("UTF-8"));
writeString(os, String.format("install_zip(\"%s%s\");", "/sdcard/", flashFilename));
writeString(os, String.format("install_zip(\"%s%s\");", "/emmc/", flashFilename));
for (String file : extras) {
os.write(String.format("install_zip(\"%s%s\");\n", "/sdcard/", file)
.getBytes("UTF-8"));
os.write(String.format("install_zip(\"%s%s\");\n", "/emmc/", file)
.getBytes("UTF-8"));
writeString(os, String.format("install_zip(\"%s%s\");", "/sdcard/", file));
writeString(os, String.format("install_zip(\"%s%s\");", "/emmc/", file));
}
os.write(("run_program(\"/sbin/busybox\", \"rm\", \"-rf\", \"/cache/*\");\n")
.getBytes("UTF-8"));
writeString(os, "run_program(\"/sbin/busybox\", \"rm\", \"-rf\", \"/cache/*\");");
} finally {
os.close();
}
setPermissions("/cache/recovery/extendedcommand", 0644, Process.myUid(), 2001 /* AID_CACHE */);
} else {
(new File("/cache/recovery/extendedcommand")).delete();
}
setPermissions("/cache/recovery/extendedcommand", 0644, Process.myUid(), 2001 /* AID_CACHE */);
((PowerManager) getSystemService(Context.POWER_SERVICE)).reboot("recovery");
} catch (Exception e) {
// We have failed to write something. There's not really anything
// else to do at
// We have failed to write something. There's not really anything else to do at
// at this stage than give up. No reason to crash though.
Logger.ex(e);
}