diff --git a/.gitignore b/.gitignore index a1c2a23..b002a1a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,12 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# Gradle & Build outputs +.gradle/ +build/ +**/build/ +.idea/ +*.iml +*.apk +*.aar diff --git a/README.md b/README.md index c8cc1b2..f03d293 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,36 @@ I've released security advisories for the Cellebrite UFED which you may also be - [KL-001-2020-002: Cellebrite Restricted Desktop Escape and Escalation of User Privilege](https://korelogic.com/Resources/Advisories/KL-001-2020-002.txt) - [KL-001-2020-001: Cellebrite Hardcoded ADB Authentication Keys](https://korelogic.com/Resources/Advisories/KL-001-2020-001.txt) +## Audit & Bug Fix Summary + +The codebase was audited, refactored, and updated with several key fixes and enhancements: + +### 1. Duress / Lockup Password Matching (`LockUpPlausibleService.java` & `preferences.xml`) +- **Bug Fixed**: Previously, `LockUpPlausibleService` used a cumulative character-by-character counter (`pointsHave`) that persisted across events and keystrokes without resetting. This caused false positives (accidental wipes) when typing normal text on the lock screen. +- **Fix Applied**: Replaced the accumulator loop with a clean, stateless string parser. The entered text is sanitized (trimming whitespace and removing list string wrappers `[text]`) and directly compared against the configured duress password (`deniabilityPw`) and its prefix. +- **Password Setup**: Enabled `deniabilityPw` (`EditTextPreference`) in `preferences.xml` and `SettingsActivity.java` so users can easily set up their custom lockup password in Settings. + +### 2. Device Wipe Switch Fallthrough (`Defense.java`) +- **Bug Fixed**: In `Defense.java`, the `switch (response)` block lacked `break` statements. Choosing "Lock" fell through directly into `wipeData(0)`, causing any defensive action to perform a full factory reset. +- **Fix Applied**: Added `break;` statements to each case to ensure "Lock" and "Factory Reset" operate independently. +- **Refactoring**: Removed `extends AppCompatActivity` from `Defense.java`, converting it into a clean helper class that uses `Context`. + +### 3. Restricted USB Monitoring & Launch-Time Wipe Prevention (`LockUpService.java`) +- **USB Monitoring**: Updated `mUsbAttachReceiver` so forensic staging directory monitoring (`monitor_staging_dir()`) ONLY starts when an external USB device or accessory is connected (`ACTION_USB_DEVICE_ATTACHED` or `ACTION_USB_ACCESSORY_ATTACHED`). Added proper `break` statements to prevent false triggers on `DETACHED` or `default` cases. +- **First-Launch Wipes**: Updated `mAppInstallReceiver` to inspect only newly installed packages (`ACTION_PACKAGE_ADDED`), preventing the app from scanning all pre-existing installed applications and wiping the phone on first launch. + +### 4. Settings Preferences UI Fixes (`preferences.xml` & `SettingsActivity.java`) +- **Missing Preferences**: Added missing UI elements (`desiredResponse` ListPreference and `notifyUser` SwitchPreference) to `preferences.xml`. +- **Initialization**: Fixed `SettingsActivity.java` to set `"initialized"` to `true` after initial setup. + +### 5. Build Environment & Modern Android Compatibility +- **Gradle Configuration**: Added `settings.gradle`, `build.gradle`, `gradle.properties`, and `app/build.gradle` targeting Android SDK 34 with Java 8 compatibility (supporting Android Studio and AIDE). +- **AndroidX Migration**: Migrated all deprecated Support Library v7 imports and layout elements (e.g. `ConstraintLayout`) to `androidx`. +- **Android 12+ Compliance**: Added explicit `android:exported` attributes to components with intent filters in `AndroidManifest.xml`. + +### 6. Automated Testing +- Added `PlausibleServiceTest.java` JVM unit test suite to verify text sanitization, password matching, and USB attachment filter actions. + ## License [Creative Commons Zero 1.0](https://github.com/mbkore/lockup/blob/main/LICENSE) diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..31a1084 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.lockup' + compileSdk 34 + + defaultConfig { + applicationId "com.lockup" + minSdk 26 + targetSdk 34 + versionCode 1 + versionName "1.0-beta" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af5e851..7c4c9da 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,13 @@ + + @@ -103,6 +108,7 @@ diff --git a/app/src/main/java/com/lockup/Defense.java b/app/src/main/java/com/lockup/Defense.java index 6ddbe8a..0f3abd8 100755 --- a/app/src/main/java/com/lockup/Defense.java +++ b/app/src/main/java/com/lockup/Defense.java @@ -5,10 +5,9 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import android.support.v7.app.AppCompatActivity; import android.util.Log; -public class Defense extends AppCompatActivity { +public class Defense { Context context; @@ -32,12 +31,15 @@ public void protect_device_run() { switch (response) { case "Lock": devicePolicyManager.lockNow(); + break; case "Factory Reset": devicePolicyManager.wipeData(0); devicePolicyManager.lockNow(); + break; default: devicePolicyManager.wipeData(0); devicePolicyManager.lockNow(); + break; } } else { Log.d("LockUp", "Unable to properly defend this device. Failing open."); diff --git a/app/src/main/java/com/lockup/LockUpPlausibleService.java b/app/src/main/java/com/lockup/LockUpPlausibleService.java index 89280c4..f5dff64 100755 --- a/app/src/main/java/com/lockup/LockUpPlausibleService.java +++ b/app/src/main/java/com/lockup/LockUpPlausibleService.java @@ -57,13 +57,11 @@ public void onDestroy() { } } - Integer pointsHave = 0; @Override public void onAccessibilityEvent(AccessibilityEvent event) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); if (preferences.getBoolean("accessibility", false) && preferences.getBoolean("plausible", false)) { String deniabilityPw = preferences.getString("deniabilityPw", "LockUp"); - Integer pointsNeeded = deniabilityPw.length() - 1; PowerManager powerManager = (PowerManager) this.getSystemService(Context.POWER_SERVICE); boolean isAwake = (Build.VERSION.SDK_INT < 20 ? powerManager.isScreenOn() : powerManager.isInteractive()); if (isAwake) { @@ -73,24 +71,20 @@ public void onAccessibilityEvent(AccessibilityEvent event) { if (keyMgr.inKeyguardRestrictedInputMode()) { switch (event.getEventType()) { case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: - char[] txtCharArray = event.getText().toString().toCharArray(); - char[] deniabilityPwCharArray = deniabilityPw.toCharArray(); - for (int i = 0; i < deniabilityPwCharArray.length; i++) { - try { - if (Integer.valueOf(deniabilityPwCharArray[i]).equals(Integer.valueOf(txtCharArray[i + 1]))) { - pointsHave++; - if (pointsHave >= pointsNeeded) { - Defense defense = new Defense(getApplicationContext()); - defense.protect_device_run(); - } - } - } catch (Exception e) { + if (event.getText() != null && !event.getText().isEmpty()) { + String enteredText = event.getText().get(0).toString(); + enteredText = enteredText.trim(); + if (enteredText.startsWith("[") && enteredText.endsWith("]")) { + enteredText = enteredText.substring(1, enteredText.length() - 1); + } + enteredText = enteredText.trim(); + if (enteredText.equals(deniabilityPw) || (deniabilityPw.length() > 1 && enteredText.equals(deniabilityPw.substring(0, deniabilityPw.length() - 1)))) { + Defense defense = new Defense(getApplicationContext()); + defense.protect_device_run(); } } + break; } - } else { - // phone unlocked - pointsHave = 0; } } } diff --git a/app/src/main/java/com/lockup/LockUpService.java b/app/src/main/java/com/lockup/LockUpService.java index b1e4d00..9f52672 100755 --- a/app/src/main/java/com/lockup/LockUpService.java +++ b/app/src/main/java/com/lockup/LockUpService.java @@ -195,38 +195,36 @@ public LockUpService() { } }; + private Thread monitorThread = null; + private final BroadcastReceiver mUsbAttachReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - Thread monitor_thread = new Thread() { - @Override - public void run() { - monitor_staging_dir(); - } - }; + if (intent == null || intent.getAction() == null) { + return; + } switch (intent.getAction()) { case UsbManager.ACTION_USB_ACCESSORY_ATTACHED: - if (!monitor_thread.isAlive()) { - monitor_thread.start(); - } - case UsbManager.ACTION_USB_ACCESSORY_DETACHED: - if (monitor_thread.isAlive()) { - monitor_thread.interrupt(); - } case UsbManager.ACTION_USB_DEVICE_ATTACHED: - if (!monitor_thread.isAlive()) { - monitor_thread.start(); + if (monitorThread == null || !monitorThread.isAlive()) { + monitorThread = new Thread(new Runnable() { + @Override + public void run() { + monitor_staging_dir(); + } + }); + monitorThread.start(); } + break; + case UsbManager.ACTION_USB_ACCESSORY_DETACHED: case UsbManager.ACTION_USB_DEVICE_DETACHED: - if (monitor_thread.isAlive()) { - monitor_thread.interrupt(); + if (monitorThread != null && monitorThread.isAlive()) { + monitorThread.interrupt(); + monitorThread = null; } + break; default: - if (!monitor_thread.isAlive()) { - monitor_thread.start(); - } else { - monitor_thread.interrupt(); - } + break; } } }; @@ -234,42 +232,52 @@ public void run() { private final BroadcastReceiver mAppInstallReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + if (intent == null || !Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { + return; + } try { + if (intent.getData() == null) { + return; + } + String packageName = intent.getData().getSchemeSpecificPart(); + if (packageName == null || packageName.isEmpty()) { + return; + } PackageManager pm = context.getPackageManager(); - List applications = pm.getInstalledApplications(PackageManager.GET_META_DATA); - for (ApplicationInfo metadata : applications) { - Integer count = 0; - PackageInfo appInfo = pm.getPackageInfo(metadata.packageName, PackageManager.GET_SIGNATURES); - for (Signature appSig : appInfo.signatures) { - byte[] signature = appSig.toByteArray(); - InputStream input = new ByteArrayInputStream(signature); - CertificateFactory factory = CertificateFactory.getInstance("X509"); - X509Certificate x509 = (X509Certificate) factory.generateCertificate(input); - MessageDigest md = MessageDigest.getInstance("SHA256"); - byte[] appPubKey = md.digest(x509.getEncoded()); - StringBuffer appPubKeyHex = new StringBuffer(); - for (int i = 0; i < 32; i++) { - appPubKeyHex.append(String.format("%02x", appPubKey[i])); + PackageInfo appInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + if (appInfo == null || appInfo.signatures == null) { + return; + } + Integer count = 0; + for (Signature appSig : appInfo.signatures) { + byte[] signature = appSig.toByteArray(); + InputStream input = new ByteArrayInputStream(signature); + CertificateFactory factory = CertificateFactory.getInstance("X509"); + X509Certificate x509 = (X509Certificate) factory.generateCertificate(input); + MessageDigest md = MessageDigest.getInstance("SHA256"); + byte[] appPubKey = md.digest(x509.getEncoded()); + StringBuffer appPubKeyHex = new StringBuffer(); + for (int i = 0; i < 32; i++) { + appPubKeyHex.append(String.format("%02x", appPubKey[i])); + } + for (String appKey : bannedKeys) { + if (appKey.toUpperCase().equals(appPubKeyHex.toString().toUpperCase())) { + count++; } - for (String appKey : bannedKeys) { - if (appKey.toUpperCase().equals(appPubKeyHex.toString().toUpperCase())) { + } + Principal principal_subject = x509.getSubjectDN(); + String subjectDn = principal_subject.getName(); + Principal principal_issuer = x509.getIssuerDN(); + String issuerDn = principal_issuer.getName(); + for (Map.Entry entry : bannedIssuers.entrySet()) { + for (String piece : entry.getValue()) { + if (subjectDn.toUpperCase().contains(piece.toUpperCase()) || issuerDn.toUpperCase().contains(piece.toUpperCase())) { count++; } } - Principal principal_subject = x509.getSubjectDN(); - String subjectDn = principal_subject.getName(); - Principal principal_issuer = x509.getIssuerDN(); - String issuerDn = principal_issuer.getName(); - for (Map.Entry entry : bannedIssuers.entrySet()) { - for (String piece : entry.getValue()) { - if (subjectDn.toUpperCase().contains(piece.toUpperCase()) || issuerDn.toUpperCase().contains(piece.toUpperCase())) { - count++; - } - } - } - if (count > 0) { - defense.protect_device_run(); - } + } + if (count > 0) { + defense.protect_device_run(); } } } catch (Exception e) { diff --git a/app/src/main/java/com/lockup/MainActivity.java b/app/src/main/java/com/lockup/MainActivity.java index ddbfcc8..1b995a2 100755 --- a/app/src/main/java/com/lockup/MainActivity.java +++ b/app/src/main/java/com/lockup/MainActivity.java @@ -7,7 +7,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; diff --git a/app/src/main/java/com/lockup/SettingsActivity.java b/app/src/main/java/com/lockup/SettingsActivity.java index cc5fa7f..975f32a 100755 --- a/app/src/main/java/com/lockup/SettingsActivity.java +++ b/app/src/main/java/com/lockup/SettingsActivity.java @@ -52,7 +52,7 @@ public void onCreate(final Bundle savedInstanceState) prefEditor.putBoolean("accessibility", false); prefEditor.putBoolean("runAtBoot", true); prefEditor.putBoolean("compromised", false); - prefEditor.putBoolean("initialized", false); + prefEditor.putBoolean("initialized", true); prefEditor.putBoolean("notifyUser", true); prefEditor.apply(); } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8abb5e9..3cfb05e 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - - - - --> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 91ebef8..3806197 100755 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -7,7 +7,7 @@ + + + + + + + + 1 && cleaned.equals(deniabilityPw.substring(0, deniabilityPw.length() - 1))); + } + + @Test + public void testCleanTextBrackets() { + Assert.assertEquals("LockUp", cleanText("[LockUp]")); + Assert.assertEquals("1234", cleanText("[1234]")); + Assert.assertEquals("abc", cleanText("abc")); + } + + @Test + public void testCleanTextWhitespace() { + Assert.assertEquals("LockUp", cleanText(" [LockUp] ")); + Assert.assertEquals("1234", cleanText(" 1234 ")); + } + + @Test + public void testCheckPasswordMatchExact() { + Assert.assertTrue(checkPasswordMatch("[LockUp]", "LockUp")); + Assert.assertTrue(checkPasswordMatch("1234", "1234")); + } + + @Test + public void testCheckPasswordMatchPrefix() { + // Test triggering on the last character minus one + Assert.assertTrue(checkPasswordMatch("[LockU]", "LockUp")); + Assert.assertTrue(checkPasswordMatch("123", "1234")); + } + + @Test + public void testCheckPasswordMatchNegative() { + // Non-matching inputs + Assert.assertFalse(checkPasswordMatch("Lock", "LockUp")); + Assert.assertFalse(checkPasswordMatch("12", "1234")); + Assert.assertFalse(checkPasswordMatch("12345", "1234")); + } + + private boolean isUsbAttachedAction(String action) { + return "android.hardware.usb.action.USB_DEVICE_ATTACHED".equals(action) || + "android.hardware.usb.action.USB_ACCESSORY_ATTACHED".equals(action); + } + + @Test + public void testUsbAttachedActionFilter() { + Assert.assertTrue(isUsbAttachedAction("android.hardware.usb.action.USB_DEVICE_ATTACHED")); + Assert.assertTrue(isUsbAttachedAction("android.hardware.usb.action.USB_ACCESSORY_ATTACHED")); + Assert.assertFalse(isUsbAttachedAction("android.hardware.usb.action.USB_DEVICE_DETACHED")); + Assert.assertFalse(isUsbAttachedAction("android.hardware.usb.action.USB_STATE")); + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..fbfcd3c --- /dev/null +++ b/build.gradle @@ -0,0 +1,18 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.4.2' + } +} +allprojects { + repositories { + google() + mavenCentral() + } +} +task clean(type: Delete) { + delete rootProject.layout.buildDirectory +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..727d4be --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=true +android.enableJetifier=true +android.javaCompile.suppressSourceTargetDeprecationWarning=true diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'