diff --git a/.github/workflows/android-lint.yml b/.github/workflows/android-lint.yml
new file mode 100644
index 000000000..3b6125ac2
--- /dev/null
+++ b/.github/workflows/android-lint.yml
@@ -0,0 +1,45 @@
+name: Android Lint
+
+on:
+ push:
+ branches: [master, dev]
+ paths:
+ - 'src/MobileShepherd/MobileShepherd/**'
+ pull_request:
+ branches: [master, dev]
+ paths:
+ - 'src/MobileShepherd/MobileShepherd/**'
+
+jobs:
+ android-lint:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: src/MobileShepherd/MobileShepherd
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v2
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Run Android Lint
+ run: ./gradlew lintDebug
+
+ - name: Upload Lint Results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: android-lint-report
+ path: src/MobileShepherd/MobileShepherd/app/build/reports/lint-results-debug.html
+ retention-days: 30
diff --git a/.github/workflows/build-and-publish-dev.yml b/.github/workflows/build-and-publish-dev.yml
new file mode 100644
index 000000000..af914c359
--- /dev/null
+++ b/.github/workflows/build-and-publish-dev.yml
@@ -0,0 +1,66 @@
+name: build-and-publish-dev
+on:
+ workflow_dispatch:
+ # Disabled for fork - requires Google Cloud credentials
+ # push:
+ # branches:
+ # - "dev"
+
+permissions:
+ contents: read
+ id-token: write
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - id: "auth"
+ name: "Authenticate to Google Cloud"
+ uses: "google-github-actions/auth@v2"
+ with:
+ token_format: "access_token"
+ workload_identity_provider: ${{secrets.WORKLOAD_IDENTITY_PROVIDER_DEV}}
+ service_account: ${{secrets.SERVICE_ACCOUNT_ID_DEV}}
+
+ - name: Login to GCR
+ uses: docker/login-action@v2
+ with:
+ registry: us-docker.pkg.dev
+ username: oauth2accesstoken
+ password: ${{ steps.auth.outputs.access_token }}
+
+ - name: Set environment variables
+ uses: c-py/action-dotenv-to-setenv@v2
+ with:
+ env-file: .env
+
+ - name: Set up JDK 1.8
+ uses: actions/setup-java@v4
+ with:
+ java-version: 8
+ distribution: 'temurin'
+ - name: Build Maven with Docker Profile
+ run: mvn clean install -Pdocker -DskipTests -B
+ - name: Build and push
+ uses: docker/bake-action@master
+ with:
+ push: true
+ env:
+ IMAGE_TOMCAT: security-shepherd:latest
+ IMAGE_MARIADB: security-shepherd_mariadb:latest
+ IMAGE_MONGO: security-shepherd_mongo:latest
+ CONTAINER_TOMCAT: secshep-tomcat
+ CONTAINER_MARIADB: secshep-mariadb
+ CONTAINER_MONGO: secshep-mongo
+ MONGO_BIND_ADDRESS: "0.0.0.0"
+ # - name: Setup tmate session
+ # uses: mxschmitt/action-tmate@v3
+ # with:
+ # limit-access-to-actor: true
+ # if: ${{ failure() }}
diff --git a/mobile/.gitignore b/mobile/.gitignore
index df1ce3d5e..79e0fdefb 100644
--- a/mobile/.gitignore
+++ b/mobile/.gitignore
@@ -1,29 +1,21 @@
-# built application files
-*.apk
-*.ap_
-
-# files for the dex VM
-*.dex
-
-# Java class files
-*.class
-
-# generated files
-bin/
-gen/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Eclipse project files
-.classpath
-.project
-
-# Proguard folder generated by Eclipse
-proguard/
-
-# Intellij project files
*.iml
-*.ipr
-*.iws
-.idea/
\ No newline at end of file
+.gradle
+/local.properties
+# Ignore all IDE project files
+.idea/
+/.idea/
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+CHALLENGE_SOLUTIONS.txt
+missing_challenges.txt
+MOBILE_SHEPHERD_WALKTHROUGH.md
+# Dev planning docs (local only)
+MATERIAL3_IMPLEMENTATION_SUMMARY.md
+MOBILE_UI_MODERNIZATION_PLAN.md
+# Environment / secrets
+.env
+.env.*
diff --git a/mobile/app/.gitignore b/mobile/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/mobile/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/mobile/app/build.gradle b/mobile/app/build.gradle
new file mode 100644
index 000000000..86ae11b3b
--- /dev/null
+++ b/mobile/app/build.gradle
@@ -0,0 +1,75 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'org.owasp.mobileshepherd'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "org.owasp.mobileshepherd"
+ minSdk 21
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ minifyEnabled false
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ testOptions {
+ unitTests {
+ returnDefaultValues = true
+ }
+ }
+ sourceSets {
+ test {
+ java {
+ // These test files reference unimplemented modules — exclude from compilation
+ // until the corresponding fragments/models are implemented.
+ exclude 'org/owasp/mobileshepherd/InsecureData3Test.java'
+ exclude 'org/owasp/mobileshepherd/ReverseEngineering2Test.java'
+ exclude 'org/owasp/mobileshepherd/ReverseEngineeringChallenge3Test.java'
+ exclude 'org/owasp/mobileshepherd/SecurityMisconfigChallenge3Test.java'
+ exclude 'org/owasp/mobileshepherd/SupplyChainChallengeTest.java'
+ }
+ }
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.preference:preference:1.2.1'
+ implementation 'com.google.android.material:material:1.11.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
+ implementation 'androidx.navigation:navigation-fragment:2.7.7'
+ implementation 'androidx.navigation:navigation-ui:2.7.7'
+ implementation 'androidx.exifinterface:exifinterface:1.3.7'
+ implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
+
+ // Unit testing
+ testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.robolectric:robolectric:4.10'
+
+ // Instrumented testing
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
\ No newline at end of file
diff --git a/mobile/app/proguard-rules.pro b/mobile/app/proguard-rules.pro
new file mode 100644
index 000000000..41f0d234e
--- /dev/null
+++ b/mobile/app/proguard-rules.pro
@@ -0,0 +1,101 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# ===================================
+# REVERSE ENGINEERING LESSONS/CHALLENGES
+# Keep these unobfuscated for educational purposes
+# ===================================
+
+# Reverse Engineering Lesson - meant to be reverse engineered
+-keep class org.owasp.mobileshepherd.ui.lessons.** { *; }
+
+# Reverse Engineering Challenges 1-3 - meant to be reverse engineered
+-keep class org.owasp.mobileshepherd.ui.challenges.** { *; }
+
+# ===================================
+# OBFUSCATE EVERYTHING ELSE
+# These should be protected from reverse engineering
+# ===================================
+
+# Keep names for ViewBinding and ViewModels to avoid runtime issues
+-keep class org.owasp.mobileshepherd.databinding.** { *; }
+-keep class * extends androidx.lifecycle.ViewModel { *; }
+
+# Keep MainActivity and navigation infrastructure
+-keep class org.owasp.mobileshepherd.MainActivity { *; }
+-keep class org.owasp.mobileshepherd.LandingActivity { *; }
+-keep class org.owasp.mobileshepherd.Preferences { *; }
+
+# Keep Fragment classes but obfuscate their internals
+-keepnames class * extends androidx.fragment.app.Fragment
+
+# Keep these challenge/lesson packages OBFUSCATED (not in RE category)
+# Insecure Data Storage - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.insecuredata.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.insecuredata1.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.insecuredata3.**
+
+# Poor Authentication - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.poorauth.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.poorauth.**
+
+# Supply Chain Security - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.supplychain.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.supplychain.**
+
+# Insecure Communication - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.insecurecomm.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.insecurecomm.**
+
+# Cryptography - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.crypto.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.crypto.**
+
+# Security Misconfiguration - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.securitymisconfig.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.securitymisconfig.**
+
+# Input Validation - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.inputvalidation.**
+-keepnames class org.owasp.mobileshepherd.ui.challenges.inputvalidation.**
+
+# Insecure Authorization - should be obfuscated
+-keepnames class org.owasp.mobileshepherd.ui.lessons.insecureauthorization.**
+
+# Home fragment
+-keepnames class org.owasp.mobileshepherd.ui.home.**
+
+# Aggressive obfuscation settings
+-optimizationpasses 5
+-overloadaggressively
+-repackageclasses ''
+-allowaccessmodification
+
+# Obfuscate string constants (except in RE packages)
+-adaptclassstrings
+
+# Remove logging for non-RE packages
+-assumenosideeffects class android.util.Log {
+ public static *** d(...);
+ public static *** v(...);
+ public static *** i(...);
+}
+
+# Keep source file and line numbers for debugging
+-keepattributes SourceFile,LineNumberTable
+-renamesourcefileattribute SourceFile
+
+# AndroidX and Material Components
+-keep class androidx.** { *; }
+-keep interface androidx.** { *; }
+-keep class com.google.android.material.** { *; }
+
+# Navigation component
+-keep class androidx.navigation.** { *; }
+
+# Prevent stripping of enum classes
+-keepclassmembers enum * { *; }
\ No newline at end of file
diff --git a/mobile/app/src/androidTest/java/com/owasp/reverser/ExampleInstrumentedTest.java b/mobile/app/src/androidTest/java/com/owasp/reverser/ExampleInstrumentedTest.java
new file mode 100644
index 000000000..15d121754
--- /dev/null
+++ b/mobile/app/src/androidTest/java/com/owasp/reverser/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package org.owasp.mobileshepherd;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("org.owasp.mobileshepherd", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/mobile/app/src/main/AndroidManifest.xml b/mobile/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..0a7792c9f
--- /dev/null
+++ b/mobile/app/src/main/AndroidManifest.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/LandingActivity.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/LandingActivity.java
new file mode 100644
index 000000000..afddda826
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/LandingActivity.java
@@ -0,0 +1,73 @@
+package org.owasp.mobileshepherd;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+public class LandingActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_landing);
+
+ Button enterAppButton = findViewById(R.id.enter_app_button);
+ enterAppButton.setOnClickListener(v -> {
+ Intent intent = new Intent(LandingActivity.this, LoginActivity.class);
+ startActivity(intent);
+ finish();
+ });
+
+ runEntranceAnimations();
+ }
+
+ private void runEntranceAnimations() {
+ TextView title = findViewById(R.id.landing_title);
+ TextView subtitle = findViewById(R.id.landing_subtitle);
+ View warningBox = findViewById(R.id.landing_warning_box);
+ TextView description = findViewById(R.id.landing_description);
+ View enterAppButton = findViewById(R.id.enter_app_button);
+ TextView license = findViewById(R.id.landing_license);
+
+ for (View v : new View[]{title, subtitle, warningBox, description, enterAppButton, license}) {
+ v.setAlpha(0f);
+ v.setTranslationY(dpToPx(40));
+ }
+
+ animateSlideUp(title, 500, 150);
+ animateSlideUp(subtitle, 450, 300);
+ animateSlideUp(warningBox, 400, 440);
+ animateSlideUp(description, 400, 560);
+
+ // Button with extra overshoot for emphasis
+ enterAppButton.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setDuration(500)
+ .setStartDelay(660)
+ .setInterpolator(new OvershootInterpolator(1.5f))
+ .start();
+
+ animateSlideUp(license, 300, 760);
+ }
+
+ private void animateSlideUp(View v, long duration, long delay) {
+ v.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setDuration(duration)
+ .setStartDelay(delay)
+ .setInterpolator(new DecelerateInterpolator())
+ .start();
+ }
+
+ private float dpToPx(int dp) {
+ return dp * getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/LoginActivity.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/LoginActivity.java
new file mode 100644
index 000000000..1a27dc562
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/LoginActivity.java
@@ -0,0 +1,152 @@
+package org.owasp.mobileshepherd;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.textfield.TextInputLayout;
+import org.owasp.mobileshepherd.utils.AuthManager;
+
+/**
+ * Full-screen login/register screen matching the Security Shepherd web app style.
+ *
+ *
Launched from {@link LandingActivity} (after the warning splash) and from
+ * {@link MainActivity} when the user taps "Sign In to Server" on the Home screen.
+ * On successful authentication the user proceeds to {@link MainActivity}.
+ * The "Continue offline" button skips authentication entirely.
+ */
+public class LoginActivity extends AppCompatActivity {
+
+ /** Extra key: when {@code true}, the user arrived from inside the app (HomeFragment). */
+ public static final String EXTRA_FROM_APP = "from_app";
+
+ private boolean isRegisterMode = false;
+
+ private TextInputEditText serverField;
+ private TextInputEditText usernameField;
+ private TextInputEditText passwordField;
+ private TextInputEditText emailField;
+ private TextInputLayout emailLayout;
+ private TextView statusText;
+ private MaterialButton tabSignIn;
+ private MaterialButton tabRegister;
+ private MaterialButton submitButton;
+ private TextView step3Text;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // If already authenticated, skip straight to the main app
+ if (AuthManager.isAuthenticated(this)) {
+ goToMain();
+ return;
+ }
+
+ setContentView(R.layout.activity_login);
+
+ serverField = findViewById(R.id.login_server_url);
+ usernameField = findViewById(R.id.login_username);
+ passwordField = findViewById(R.id.login_password);
+ emailField = findViewById(R.id.login_email);
+ emailLayout = findViewById(R.id.login_email_layout);
+ statusText = findViewById(R.id.login_status_text);
+ tabSignIn = findViewById(R.id.tab_sign_in);
+ tabRegister = findViewById(R.id.tab_register);
+ submitButton = findViewById(R.id.login_submit_button);
+ step3Text = findViewById(R.id.login_step3_text);
+
+ // Pre-fill saved credentials so returning users don't have to re-enter everything
+ String savedServer = AuthManager.getServerUrl(this);
+ if (!savedServer.isEmpty()) serverField.setText(savedServer);
+ String savedUser = AuthManager.getUsername(this);
+ if (!savedUser.isEmpty()) usernameField.setText(savedUser);
+
+ tabSignIn.setOnClickListener(v -> setMode(false));
+ tabRegister.setOnClickListener(v -> setMode(true));
+ submitButton.setOnClickListener(v -> onSubmit());
+
+ boolean fromApp = getIntent().getBooleanExtra(EXTRA_FROM_APP, false);
+ MaterialButton offlineButton = findViewById(R.id.login_offline_button);
+ if (fromApp) {
+ offlineButton.setText(R.string.cancel);
+ }
+ offlineButton.setOnClickListener(v -> {
+ // Offline mode: go back to the main app (or finish if launched from within it)
+ goToMain();
+ });
+
+ setMode(false);
+ }
+
+ private void setMode(boolean registerMode) {
+ isRegisterMode = registerMode;
+ if (registerMode) {
+ emailLayout.setVisibility(View.VISIBLE);
+ submitButton.setText(R.string.auth_dialog_title_register);
+ step3Text.setText(R.string.login_step3_register);
+ } else {
+ emailLayout.setVisibility(View.GONE);
+ submitButton.setText(R.string.auth_button_sign_in);
+ step3Text.setText(R.string.login_step3_signin);
+ }
+ statusText.setVisibility(View.GONE);
+ }
+
+ private void onSubmit() {
+ String serverUrl = serverField.getText() != null ? serverField.getText().toString().trim() : "";
+ String username = usernameField.getText() != null ? usernameField.getText().toString().trim() : "";
+ String password = passwordField.getText() != null ? passwordField.getText().toString() : "";
+ String email = emailField.getText() != null ? emailField.getText().toString().trim() : "";
+
+ if (TextUtils.isEmpty(serverUrl) || TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
+ showStatus(getString(R.string.login_error_fill_fields), true);
+ return;
+ }
+
+ submitButton.setEnabled(false);
+ tabSignIn.setEnabled(false);
+ tabRegister.setEnabled(false);
+ showStatus(getString(R.string.login_connecting), false);
+
+ AuthManager.AuthCallback callback = (success, message) -> {
+ if (success) {
+ goToMain();
+ } else {
+ submitButton.setEnabled(true);
+ tabSignIn.setEnabled(true);
+ tabRegister.setEnabled(true);
+ showStatus(message, true);
+ }
+ };
+
+ if (isRegisterMode) {
+ AuthManager.register(this, serverUrl, username, password, email, callback);
+ } else {
+ AuthManager.login(this, serverUrl, username, password, callback);
+ }
+ }
+
+ private void showStatus(String message, boolean isError) {
+ statusText.setText(message);
+ statusText.setTextColor(isError
+ ? getResources().getColor(android.R.color.holo_red_dark, getTheme())
+ : getResources().getColor(android.R.color.darker_gray, getTheme()));
+ statusText.setVisibility(View.VISIBLE);
+ }
+
+ private void goToMain() {
+ boolean fromApp = getIntent().getBooleanExtra(EXTRA_FROM_APP, false);
+ if (!fromApp) {
+ Intent intent = new Intent(this, MainActivity.class);
+ startActivity(intent);
+ }
+ finish();
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/MainActivity.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/MainActivity.java
new file mode 100644
index 000000000..4d9b9dfb1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/MainActivity.java
@@ -0,0 +1,501 @@
+package org.owasp.mobileshepherd;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.MenuItem;
+import android.view.Menu;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.android.material.navigation.NavigationView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.PreferenceManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.snackbar.Snackbar;
+import org.owasp.mobileshepherd.databinding.ActivityMainBinding;
+import org.owasp.mobileshepherd.utils.AuthManager;
+import org.owasp.mobileshepherd.utils.FlagValidator;
+import org.owasp.mobileshepherd.utils.ProgressTracker;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MainActivity extends AppCompatActivity {
+
+ private AppBarConfiguration mAppBarConfiguration;
+ private ActivityMainBinding binding;
+ private ProgressTracker progressTracker;
+ private NavigationAdapter navigationAdapter;
+ private static final Map NAV_TO_MODULE_MAP = new HashMap<>();
+
+ static {
+ // Lessons
+ NAV_TO_MODULE_MAP.put(R.id.nav_lesson, FlagValidator.Module.RE_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_insecure_data_lesson, FlagValidator.Module.IDS_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_poor_auth_lesson, FlagValidator.Module.POOR_AUTH_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_insecure_authorization_lesson, FlagValidator.Module.INSECURE_AUTH_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_supply_chain_lesson, FlagValidator.Module.SUPPLY_CHAIN_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_insecure_comm_lesson, FlagValidator.Module.INSECURE_COMM_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_insufficient_crypto_lesson, FlagValidator.Module.INSUFFICIENT_CRYPTO_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_security_misconfig_lesson, FlagValidator.Module.SECURITY_MISCONFIG_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_input_validation_lesson, FlagValidator.Module.INPUT_VALIDATION_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_privacy_lesson, FlagValidator.Module.PRIVACY_LESSON);
+ NAV_TO_MODULE_MAP.put(R.id.nav_client_side_injection_lesson, FlagValidator.Module.CLIENT_SIDE_INJECTION_LESSON);
+
+ // Challenges
+ NAV_TO_MODULE_MAP.put(R.id.nav_challenge1, FlagValidator.Module.RE_CHALLENGE_1);
+ NAV_TO_MODULE_MAP.put(R.id.nav_insecure_data1, FlagValidator.Module.IDS_CHALLENGE_1);
+ NAV_TO_MODULE_MAP.put(R.id.nav_poor_auth_challenge, FlagValidator.Module.POOR_AUTH_CHALLENGE);
+ NAV_TO_MODULE_MAP.put(R.id.nav_insecure_comm_challenge, FlagValidator.Module.INSECURE_COMM_CHALLENGE);
+ NAV_TO_MODULE_MAP.put(R.id.nav_insufficient_crypto_challenge, FlagValidator.Module.INSUFFICIENT_CRYPTO_CHALLENGE);
+ NAV_TO_MODULE_MAP.put(R.id.nav_security_misconfig_challenge2, FlagValidator.Module.SECURITY_MISCONFIG_CHALLENGE_2);
+ NAV_TO_MODULE_MAP.put(R.id.nav_client_side_injection_challenge1, FlagValidator.Module.CLIENT_SIDE_INJECTION_CHALLENGE_1);
+ NAV_TO_MODULE_MAP.put(R.id.nav_client_side_injection_challenge2, FlagValidator.Module.CLIENT_SIDE_INJECTION_CHALLENGE_2);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ // Apply saved theme before calling super.onCreate
+ applyTheme();
+
+ super.onCreate(savedInstanceState);
+
+ binding = ActivityMainBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setSupportActionBar(binding.appBarMain.toolbar);
+
+ // Add scale animation to FAB on click
+ binding.appBarMain.fab.setScaleX(1f);
+ binding.appBarMain.fab.setScaleY(1f);
+
+ binding.appBarMain.fab.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View view) {
+ // Animate FAB scale on click
+ view.animate()
+ .scaleX(0.9f)
+ .scaleY(0.9f)
+ .setDuration(100)
+ .withEndAction(() -> {
+ view.animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(100)
+ .start();
+ })
+ .start();
+
+ //create alert dialogue for floating "help" action button.
+ AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
+
+ //set the message
+ String message = "Mobile Shepherd - Security Training Platform\n\n" +
+ "This application is designed to teach mobile application security through " +
+ "hands-on lessons and challenges based on the OWASP Mobile Top 10.\n\n" +
+ "Part of the OWASP Security Shepherd project.";
+
+ builder.setMessage(message);
+ builder.setTitle("About Mobile Shepherd");
+ builder.setCancelable(true);
+
+ builder.setPositiveButton("OWASP Mobile Top 10", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW,
+ android.net.Uri.parse("https://owasp.org/www-project-mobile-top-10/"));
+ startActivity(browserIntent);
+ }
+ });
+
+ builder.setNeutralButton("Security Shepherd", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW,
+ android.net.Uri.parse("https://owasp.org/www-project-security-shepherd/"));
+ startActivity(browserIntent);
+ }
+ });
+
+ builder.setNegativeButton("Close", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+
+ AlertDialog alertDialog = builder.create();
+ alertDialog.show();
+ }
+ });
+
+ DrawerLayout drawer = binding.drawerLayout;
+
+ // Wire up nav header auth UI (header is included directly in nav_drawer_layout)
+ TextView authStatus = findViewById(R.id.nav_header_auth_status);
+ Button authButton = findViewById(R.id.nav_header_auth_button);
+ updateNavHeader(authStatus, authButton);
+ authButton.setOnClickListener(v -> {
+ if (AuthManager.isAuthenticated(this)) {
+ AuthManager.logout(this);
+ updateNavHeader(authStatus, authButton);
+ refreshNavigation();
+ Toast.makeText(this, "Signed out", Toast.LENGTH_SHORT).show();
+ } else {
+ showAuthDialog(authStatus, authButton);
+ }
+ });
+ // Refresh header when drawer opens
+ drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ updateNavHeader(authStatus, authButton);
+ }
+ });
+
+ // Initialize ProgressTracker
+ progressTracker = new ProgressTracker(this);
+
+ // Set up completion change listener to refresh navigation
+ ProgressTracker.setGlobalCompletionListener(() -> {
+ runOnUiThread(() -> refreshNavigation());
+ });
+
+ // Setup RecyclerView for navigation
+ RecyclerView navRecyclerView = findViewById(R.id.nav_recycler_view);
+ navRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+
+ List navigationItems = createNavigationItems();
+ navigationAdapter = new NavigationAdapter(navigationItems, item -> {
+ // Handle navigation item click
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
+ navController.navigate(item.getNavigationId());
+ drawer.closeDrawers();
+ }, progressTracker, NAV_TO_MODULE_MAP);
+ navRecyclerView.setAdapter(navigationAdapter);
+
+ // Passing each menu ID as a set of Ids because each
+ // menu should be considered as top level destinations.
+ mAppBarConfiguration = new AppBarConfiguration.Builder(
+ R.id.nav_home, R.id.nav_lesson, R.id.nav_insecure_data_lesson, R.id.nav_poor_auth_lesson, R.id.nav_insecure_authorization_lesson, R.id.nav_supply_chain_lesson, R.id.nav_insecure_comm_lesson, R.id.nav_insufficient_crypto_lesson, R.id.nav_security_misconfig_lesson,
+ R.id.nav_challenge1,
+ R.id.nav_insecure_data1,
+ R.id.nav_poor_auth_challenge, R.id.nav_insecure_comm_challenge, R.id.nav_insufficient_crypto_challenge,
+ R.id.nav_security_misconfig_challenge2,
+ R.id.nav_input_validation_lesson,
+ R.id.nav_privacy_lesson,
+ R.id.nav_client_side_injection_lesson, R.id.nav_client_side_injection_challenge1, R.id.nav_client_side_injection_challenge2,
+ R.id.nav_adb_reference, R.id.nav_scoreboard
+ ).setOpenableLayout(drawer)
+ .build();
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
+ NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
+
+ // Hide FAB on the Home screen; show it on all other destinations
+ navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
+ if (destination.getId() == R.id.nav_home) {
+ binding.appBarMain.fab.hide();
+ } else {
+ binding.appBarMain.fab.show();
+ }
+ });
+ }
+
+ private List createNavigationItems() {
+ List items = new ArrayList<>();
+
+ // Home
+ items.add(new NavigationItem(1, "Home", R.drawable.ic_menu_home, R.id.nav_home));
+
+ // Lessons group
+ NavigationItem lessonsGroup = new NavigationItem(2, "Lessons", R.drawable.ic_menu_camera);
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(21, "Reverse Engineering", 0, R.id.nav_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(22, "Insecure Data Storage", 0, R.id.nav_insecure_data_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(23, "Poor Authentication", 0, R.id.nav_poor_auth_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(24, "Insecure Authorization", 0, R.id.nav_insecure_authorization_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(25, "Supply Chain Security", 0, R.id.nav_supply_chain_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(26, "Insecure Communication", 0, R.id.nav_insecure_comm_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(27, "Insufficient Cryptography", 0, R.id.nav_insufficient_crypto_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(28, "Security Misconfiguration", 0, R.id.nav_security_misconfig_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(29, "Input Validation", 0, R.id.nav_input_validation_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(30, "Privacy Controls", 0, R.id.nav_privacy_lesson));
+ addChildIfNotCompleted(lessonsGroup, new NavigationItem(31, "Client-Side Injection", 0, R.id.nav_client_side_injection_lesson));
+ if (lessonsGroup.getChildren().size() > 0) items.add(lessonsGroup);
+
+ // Challenges group
+ NavigationItem challengesGroup = new NavigationItem(3, "Challenges", R.drawable.ic_menu_code);
+
+ // Reverse Engineering sub-group
+ NavigationItem reverseEngGroup = new NavigationItem(40, "Reverse Engineering", 0);
+ addChildIfNotCompleted(reverseEngGroup, new NavigationItem(41, "Challenge 1", 0, R.id.nav_challenge1));
+ if (reverseEngGroup.getChildren().size() > 0) challengesGroup.addChild(reverseEngGroup);
+
+ // Insecure Data Storage sub-group
+ NavigationItem insecureDataGroup = new NavigationItem(44, "Insecure Data Storage", 0);
+ addChildIfNotCompleted(insecureDataGroup, new NavigationItem(45, "Challenge 1", 0, R.id.nav_insecure_data1));
+ if (insecureDataGroup.getChildren().size() > 0) challengesGroup.addChild(insecureDataGroup);
+
+ // Individual challenges
+ addChildIfNotCompleted(challengesGroup, new NavigationItem(47, "Poor Authentication", 0, R.id.nav_poor_auth_challenge));
+ addChildIfNotCompleted(challengesGroup, new NavigationItem(49, "Insecure Communication", 0, R.id.nav_insecure_comm_challenge));
+ addChildIfNotCompleted(challengesGroup, new NavigationItem(50, "Insufficient Cryptography", 0, R.id.nav_insufficient_crypto_challenge));
+
+ // Security Misconfiguration sub-group
+ NavigationItem securityMisconfigGroup = new NavigationItem(51, "Security Misconfiguration", 0);
+ addChildIfNotCompleted(securityMisconfigGroup, new NavigationItem(52, "Challenge 1", 0, R.id.nav_security_misconfig_challenge2));
+ if (securityMisconfigGroup.getChildren().size() > 0) challengesGroup.addChild(securityMisconfigGroup);
+
+ // Client-Side Injection sub-group
+ NavigationItem clientSideGroup = new NavigationItem(55, "Client-Side Injection", 0);
+ addChildIfNotCompleted(clientSideGroup, new NavigationItem(56, "Challenge 1", 0, R.id.nav_client_side_injection_challenge1));
+ addChildIfNotCompleted(clientSideGroup, new NavigationItem(57, "Challenge 2", 0, R.id.nav_client_side_injection_challenge2));
+ if (clientSideGroup.getChildren().size() > 0) challengesGroup.addChild(clientSideGroup);
+
+ if (challengesGroup.getChildren().size() > 0) items.add(challengesGroup);
+
+ // ADB Reference
+ items.add(new NavigationItem(4, "ADB Reference", R.drawable.ic_menu_code, R.id.nav_adb_reference));
+
+ // Scoreboard — only shown when signed in to a server
+ if (AuthManager.isAuthenticated(this)) {
+ items.add(new NavigationItem(6, "Scoreboard", R.drawable.ic_menu_home, R.id.nav_scoreboard));
+ }
+
+ // Completed group - show all completed lessons and challenges
+ NavigationItem completedGroup = new NavigationItem(5, "Completed", R.drawable.ic_menu_camera);
+ boolean hasCompleted = false;
+
+ for (Map.Entry entry : NAV_TO_MODULE_MAP.entrySet()) {
+ if (progressTracker.isCompleted(entry.getValue())) {
+ hasCompleted = true;
+ String title = getTitleForNavId(entry.getKey());
+ completedGroup.addChild(new NavigationItem(1000 + entry.getKey(), title, 0, entry.getKey()));
+ }
+ }
+
+ if (hasCompleted) {
+ items.add(completedGroup);
+ }
+
+ return items;
+ }
+
+ private String getTitleForNavId(int navId) {
+ // Map navigation IDs to readable titles
+ if (navId == R.id.nav_lesson) return "Reverse Engineering";
+ if (navId == R.id.nav_insecure_data_lesson) return "Insecure Data Storage";
+ if (navId == R.id.nav_poor_auth_lesson) return "Poor Authentication";
+ if (navId == R.id.nav_insecure_authorization_lesson) return "Insecure Authorization";
+ if (navId == R.id.nav_supply_chain_lesson) return "Supply Chain Security";
+ if (navId == R.id.nav_insecure_comm_lesson) return "Insecure Communication";
+ if (navId == R.id.nav_insufficient_crypto_lesson) return "Insufficient Cryptography";
+ if (navId == R.id.nav_security_misconfig_lesson) return "Security Misconfiguration";
+ if (navId == R.id.nav_input_validation_lesson) return "Input Validation";
+ if (navId == R.id.nav_privacy_lesson) return "Privacy Controls";
+ if (navId == R.id.nav_client_side_injection_lesson) return "Client-Side Injection";
+ if (navId == R.id.nav_challenge1) return "RE Challenge 1";
+ if (navId == R.id.nav_insecure_data1) return "IDS Challenge 1";
+ if (navId == R.id.nav_poor_auth_challenge) return "Poor Auth Challenge";
+ if (navId == R.id.nav_insecure_comm_challenge) return "Insecure Comm Challenge";
+ if (navId == R.id.nav_insufficient_crypto_challenge) return "Crypto Challenge";
+ if (navId == R.id.nav_security_misconfig_challenge2) return "Security Misconfig Challenge";
+ if (navId == R.id.nav_client_side_injection_challenge1) return "Client Injection Challenge 1";
+ if (navId == R.id.nav_client_side_injection_challenge2) return "Client Injection Challenge 2";
+ return "Module";
+ }
+
+ private void addChildIfNotCompleted(NavigationItem parent, NavigationItem child) {
+ // Only add the child if it's not completed
+ if (child.getNavigationId() != 0) {
+ FlagValidator.Module module = NAV_TO_MODULE_MAP.get(child.getNavigationId());
+ if (module != null && progressTracker.isCompleted(module)) {
+ return; // Skip completed items
+ }
+ }
+ parent.addChild(child);
+ }
+
+ public void refreshNavigation() {
+ List navigationItems = createNavigationItems();
+ navigationAdapter.updateItems(navigationItems);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch(item.getItemId()) {
+ case R.id.action_adb_reference: {
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
+ navController.navigate(R.id.nav_adb_reference);
+ }
+ return true;
+ case R.id.action_settings: {
+ Intent goToSettings = new Intent(this, Preferences.class);
+ startActivity(goToSettings);
+ Toast.makeText(this, "Settings Selected", Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ case R.id.action_exit:
+ finish();
+ return true;
+
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
+ return NavigationUI.navigateUp(navController, mAppBarConfiguration)
+ || super.onSupportNavigateUp();
+ }
+
+ private void updateNavHeader(TextView statusView, Button button) {
+ if (AuthManager.isAuthenticated(this)) {
+ String username = AuthManager.getUsername(this);
+ statusView.setText(getString(R.string.auth_status_online, username));
+ button.setText(R.string.auth_button_sign_out);
+ } else {
+ statusView.setText(R.string.auth_status_offline);
+ button.setText(R.string.auth_button_sign_in);
+ }
+ }
+
+ /** Called by HomeFragment to open the sign-in dialog from the home screen card. */
+ public void openAuthDialog() {
+ TextView authStatus = findViewById(R.id.nav_header_auth_status);
+ Button authButton = findViewById(R.id.nav_header_auth_button);
+ showAuthDialog(authStatus, authButton);
+ }
+
+ private void showAuthDialog(TextView statusView, Button button) {
+ final boolean[] isRegisterMode = {false};
+
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_auth, null);
+ EditText serverField = dialogView.findViewById(R.id.auth_server_url);
+ EditText usernameField = dialogView.findViewById(R.id.auth_username);
+ EditText passwordField = dialogView.findViewById(R.id.auth_password);
+ EditText emailField = dialogView.findViewById(R.id.auth_email);
+ View emailLabel = dialogView.findViewById(R.id.auth_email_label);
+ TextView statusText = dialogView.findViewById(R.id.auth_status_text);
+
+ // Pre-fill saved server URL
+ String savedServer = AuthManager.getServerUrl(this);
+ if (!savedServer.isEmpty()) serverField.setText(savedServer);
+ String savedUser = AuthManager.getUsername(this);
+ if (!savedUser.isEmpty()) usernameField.setText(savedUser);
+
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.auth_dialog_title_login)
+ .setView(dialogView)
+ .setPositiveButton(R.string.auth_button_sign_in, null) // overridden below
+ .setNeutralButton(R.string.auth_switch_to_register, null)
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+
+ dialog.setOnShowListener(d -> {
+ Button positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ Button neutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
+
+ positive.setOnClickListener(v -> {
+ String serverUrl = serverField.getText().toString().trim();
+ String username = usernameField.getText().toString().trim();
+ String password = passwordField.getText().toString();
+ String email = emailField.getText().toString().trim();
+
+ if (TextUtils.isEmpty(serverUrl) || TextUtils.isEmpty(username)
+ || TextUtils.isEmpty(password)) {
+ statusText.setText("Please fill in all required fields");
+ statusText.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ positive.setEnabled(false);
+ neutral.setEnabled(false);
+ statusText.setText("Connecting...");
+ statusText.setVisibility(View.VISIBLE);
+
+ AuthManager.AuthCallback callback = (success, message) -> {
+ if (success) {
+ updateNavHeader(statusView, button);
+ refreshNavigation();
+ dialog.dismiss();
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ } else {
+ positive.setEnabled(true);
+ neutral.setEnabled(true);
+ statusText.setText(message);
+ }
+ };
+
+ if (isRegisterMode[0]) {
+ AuthManager.register(this, serverUrl, username, password, email, callback);
+ } else {
+ AuthManager.login(this, serverUrl, username, password, callback);
+ }
+ });
+
+ neutral.setOnClickListener(v -> {
+ isRegisterMode[0] = !isRegisterMode[0];
+ if (isRegisterMode[0]) {
+ dialog.setTitle(getString(R.string.auth_dialog_title_register));
+ positive.setText(R.string.auth_dialog_title_register);
+ neutral.setText(R.string.auth_switch_to_login);
+ emailField.setVisibility(View.VISIBLE);
+ emailLabel.setVisibility(View.VISIBLE);
+ } else {
+ dialog.setTitle(getString(R.string.auth_dialog_title_login));
+ positive.setText(R.string.auth_button_sign_in);
+ neutral.setText(R.string.auth_switch_to_register);
+ emailField.setVisibility(View.GONE);
+ emailLabel.setVisibility(View.GONE);
+ }
+ statusText.setVisibility(View.GONE);
+ });
+ });
+
+ dialog.show();
+ }
+
+ private void applyTheme() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+ String themeValue = preferences.getString("theme_preference", "system");
+
+ switch (themeValue) {
+ case "light":
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
+ break;
+ case "dark":
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
+ break;
+ case "system":
+ default:
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/NavigationAdapter.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/NavigationAdapter.java
new file mode 100644
index 000000000..f8135820a
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/NavigationAdapter.java
@@ -0,0 +1,173 @@
+package org.owasp.mobileshepherd;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.owasp.mobileshepherd.utils.FlagValidator;
+import org.owasp.mobileshepherd.utils.ProgressTracker;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class NavigationAdapter extends RecyclerView.Adapter {
+
+ private List items;
+ private List displayedItems;
+ private OnItemClickListener listener;
+ private ProgressTracker progressTracker;
+ private Map navToModuleMap;
+
+ public interface OnItemClickListener {
+ void onItemClick(NavigationItem item);
+ }
+
+ public NavigationAdapter(List items, OnItemClickListener listener,
+ ProgressTracker progressTracker, Map navToModuleMap) {
+ this.items = items;
+ this.displayedItems = new ArrayList<>();
+ this.listener = listener;
+ this.progressTracker = progressTracker;
+ this.navToModuleMap = navToModuleMap;
+ updateDisplayedItems();
+ }
+
+ private void updateDisplayedItems() {
+ displayedItems.clear();
+ for (NavigationItem item : items) {
+ addItemWithChildren(item, 0);
+ }
+ }
+
+ public void updateItems(List newItems) {
+ this.items = newItems;
+ updateDisplayedItems();
+ notifyDataSetChanged();
+ }
+
+ private void addItemWithChildren(NavigationItem item, int depth) {
+ displayedItems.add(item);
+ if (item.isExpandable() && item.isExpanded()) {
+ for (NavigationItem child : item.getChildren()) {
+ addItemWithChildren(child, depth + 1);
+ }
+ }
+ }
+
+ private int getItemDepth(NavigationItem item) {
+ return getItemDepth(item, items, 0);
+ }
+
+ private int getItemDepth(NavigationItem target, List itemList, int currentDepth) {
+ for (NavigationItem item : itemList) {
+ if (item == target) {
+ return currentDepth;
+ }
+ if (item.isExpandable()) {
+ int childDepth = getItemDepth(target, item.getChildren(), currentDepth + 1);
+ if (childDepth != -1) {
+ return childDepth;
+ }
+ }
+ }
+ return -1;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.nav_item_layout, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ NavigationItem item = displayedItems.get(position);
+ holder.bind(item);
+ }
+
+ @Override
+ public int getItemCount() {
+ return displayedItems.size();
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder {
+ private ImageView icon;
+ private TextView title;
+ private ImageView expandIcon;
+ private TextView completedIndicator;
+ private View itemView;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ this.itemView = itemView;
+ icon = itemView.findViewById(R.id.nav_item_icon);
+ title = itemView.findViewById(R.id.nav_item_title);
+ expandIcon = itemView.findViewById(R.id.nav_item_expand_icon);
+ completedIndicator = itemView.findViewById(R.id.nav_item_completed_indicator);
+ }
+
+ public void bind(NavigationItem item) {
+ title.setText(item.getTitle());
+
+ // Check if this item is completed
+ boolean isCompleted = false;
+ if (item.getNavigationId() != 0 && navToModuleMap != null) {
+ FlagValidator.Module module = navToModuleMap.get(item.getNavigationId());
+ if (module != null && progressTracker != null) {
+ isCompleted = progressTracker.isCompleted(module);
+ }
+ }
+
+ // Show/hide completed indicator
+ if (isCompleted && !item.isExpandable()) {
+ completedIndicator.setVisibility(View.VISIBLE);
+ } else {
+ completedIndicator.setVisibility(View.GONE);
+ }
+
+ int depth = getItemDepth(item);
+ int leftPadding = 16 + (depth * 32); // 16dp base, 32dp per level
+
+ itemView.setPaddingRelative(
+ leftPadding,
+ itemView.getPaddingTop(),
+ itemView.getPaddingEnd(),
+ itemView.getPaddingBottom()
+ );
+
+ // Show icon only for top-level items (depth 0)
+ if (depth == 0 && item.getIconResId() != 0) {
+ icon.setImageResource(item.getIconResId());
+ icon.setVisibility(View.VISIBLE);
+ } else {
+ icon.setVisibility(View.GONE);
+ }
+
+ // Show expand icon for expandable items at any level
+ if (item.isExpandable()) {
+ expandIcon.setVisibility(View.VISIBLE);
+ expandIcon.setRotation(item.isExpanded() ? 180 : 0);
+ } else {
+ expandIcon.setVisibility(View.GONE);
+ }
+
+ itemView.setOnClickListener(v -> {
+ if (item.isExpandable()) {
+ item.setExpanded(!item.isExpanded());
+ updateDisplayedItems();
+ notifyDataSetChanged();
+ } else {
+ listener.onItemClick(item);
+ }
+ });
+ }
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/NavigationItem.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/NavigationItem.java
new file mode 100644
index 000000000..aa15cf8f1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/NavigationItem.java
@@ -0,0 +1,66 @@
+package org.owasp.mobileshepherd;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NavigationItem {
+ private int id;
+ private String title;
+ private int iconResId;
+ private boolean isExpandable;
+ private boolean isExpanded;
+ private List children;
+ private int navigationId; // For navigation component
+
+ public NavigationItem(int id, String title, int iconResId, int navigationId) {
+ this.id = id;
+ this.title = title;
+ this.iconResId = iconResId;
+ this.navigationId = navigationId;
+ this.isExpandable = false;
+ this.isExpanded = false;
+ this.children = new ArrayList<>();
+ }
+
+ public NavigationItem(int id, String title, int iconResId) {
+ this(id, title, iconResId, -1);
+ this.isExpandable = true;
+ }
+
+ public void addChild(NavigationItem child) {
+ this.children.add(child);
+ this.isExpandable = true;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public int getIconResId() {
+ return iconResId;
+ }
+
+ public boolean isExpandable() {
+ return isExpandable;
+ }
+
+ public boolean isExpanded() {
+ return isExpanded;
+ }
+
+ public void setExpanded(boolean expanded) {
+ isExpanded = expanded;
+ }
+
+ public List getChildren() {
+ return children;
+ }
+
+ public int getNavigationId() {
+ return navigationId;
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/Preferences.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/Preferences.java
new file mode 100644
index 000000000..cbcfcc2a0
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/Preferences.java
@@ -0,0 +1,131 @@
+package org.owasp.mobileshepherd;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.preference.EditTextPreference;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceManager;
+import org.owasp.mobileshepherd.utils.ProgressTracker;
+
+/**
+ * This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
The Security Shepherd project is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ *
+ * @author Sean Duggan
+ */
+public class Preferences extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Enable back button in action bar
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(true);
+ }
+
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(android.R.id.content, new PrefsFragment())
+ .commit();
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ finish();
+ return true;
+ }
+
+ public static class PrefsFragment extends PreferenceFragmentCompat {
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ // Load the preferences from an XML resource
+ setPreferencesFromResource(R.xml.preferences, rootKey);
+
+ // Set up theme preference listener
+ ListPreference themePreference = findPreference("theme_preference");
+ if (themePreference != null) {
+ themePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String themeValue = (String) newValue;
+ applyTheme(themeValue);
+ return true;
+ }
+ });
+ }
+
+ // Set up submit issue preference listener
+ Preference submitIssuePreference = findPreference("submit_issue_preference");
+ if (submitIssuePreference != null) {
+ submitIssuePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ String issuesUrl = "https://github.com/OWASP/SecurityShepherd/issues";
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(issuesUrl));
+ startActivity(browserIntent);
+ return true;
+ }
+ });
+ }
+
+ // Set up reset progress preference listener
+ Preference resetProgressPreference = findPreference("reset_progress_preference");
+ if (resetProgressPreference != null) {
+ resetProgressPreference.setOnPreferenceClickListener(preference -> {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Reset All Progress")
+ .setMessage("This will mark all lessons and challenges as incomplete. This cannot be undone.\n\nAre you sure?")
+ .setPositiveButton("Reset", (dialog, which) -> {
+ ProgressTracker tracker = new ProgressTracker(requireContext());
+ tracker.resetProgress();
+ ProgressTracker.CompletionChangeListener listener =
+ ProgressTracker.getGlobalCompletionListener();
+ if (listener != null) {
+ listener.onCompletionChanged();
+ }
+ android.widget.Toast.makeText(
+ requireContext(),
+ "All progress has been reset",
+ android.widget.Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ return true;
+ });
+ }
+ }
+
+ private void applyTheme(String themeValue) {
+ switch (themeValue) {
+ case "light":
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
+ break;
+ case "dark":
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
+ break;
+ case "system":
+ default:
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/adb/AdbReferenceFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/adb/AdbReferenceFragment.java
new file mode 100644
index 000000000..2f554c85e
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/adb/AdbReferenceFragment.java
@@ -0,0 +1,74 @@
+package org.owasp.mobileshepherd.ui.adb;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import org.owasp.mobileshepherd.databinding.FragmentAdbReferenceBinding;
+
+public class AdbReferenceFragment extends Fragment {
+
+ private FragmentAdbReferenceBinding binding;
+
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+
+ binding = FragmentAdbReferenceBinding.inflate(inflater, container, false);
+ View root = binding.getRoot();
+
+ // Setup copy buttons for each command category
+ setupCopyButton(binding.copyLogcat, "adb logcat");
+ setupCopyButton(binding.copyLogcatFilter, "adb logcat -s TAG_NAME");
+ setupCopyButton(binding.copyLogcatGrep, "adb logcat | grep \"search_term\"");
+ setupCopyButton(binding.copyLogcatClear, "adb logcat -c");
+
+ setupCopyButton(binding.copyListDevices, "adb devices");
+ setupCopyButton(binding.copyShell, "adb shell");
+ setupCopyButton(binding.copyInstall, "adb install app.apk");
+ setupCopyButton(binding.copyUninstall, "adb uninstall com.package.name");
+
+ setupCopyButton(binding.copyPullFile, "adb pull /data/data/org.owasp.mobileshepherd/databases/Users.db");
+ setupCopyButton(binding.copyPullSharedPrefs, "adb pull /data/data/org.owasp.mobileshepherd/shared_prefs/");
+ setupCopyButton(binding.copyListFiles, "adb shell ls /data/data/org.owasp.mobileshepherd/");
+ setupCopyButton(binding.copyListDatabases, "adb shell ls /data/data/org.owasp.mobileshepherd/databases/");
+
+ setupCopyButton(binding.copySqlite, "adb shell sqlite3 /data/data/org.owasp.mobileshepherd/databases/Users.db");
+ setupCopyButton(binding.copySqliteQuery, "sqlite> SELECT * FROM users;");
+ setupCopyButton(binding.copyCat, "adb shell cat /data/data/org.owasp.mobileshepherd/shared_prefs/UserCredentials.xml");
+
+ setupCopyButton(binding.copyGetPackages, "adb shell pm list packages | grep reverser");
+ setupCopyButton(binding.copyPackagePath, "adb shell pm path org.owasp.mobileshepherd");
+ setupCopyButton(binding.copyPullApk, "adb pull /data/app/org.owasp.mobileshepherd-*/base.apk");
+ setupCopyButton(binding.copyClearData, "adb shell pm clear org.owasp.mobileshepherd");
+
+ setupCopyButton(binding.copyRunAsVault, "adb shell run-as org.owasp.mobileshepherd cat shared_prefs/recovery_vault.xml");
+ setupCopyButton(binding.copyPullVault, "adb pull /data/data/org.owasp.mobileshepherd/shared_prefs/recovery_vault.xml");
+ setupCopyButton(binding.copyJadx, "jadx-gui app-debug.apk");
+ setupCopyButton(binding.copyXorDecode, "python3 -c \"import base64; d=base64.b64decode('PASTE_BASE64_HERE'); print(''.join(chr(b^0x42) for b in d))\"");
+
+ return root;
+ }
+
+ private void setupCopyButton(View button, String command) {
+ button.setOnClickListener(v -> {
+ ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("ADB Command", command);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(getContext(), "Copied to clipboard!", Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/challenges/clientsideinjection/ClientSideInjectionChallenge1Fragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/challenges/clientsideinjection/ClientSideInjectionChallenge1Fragment.java
new file mode 100644
index 000000000..64624ccd1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/challenges/clientsideinjection/ClientSideInjectionChallenge1Fragment.java
@@ -0,0 +1,258 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.preference.PreferenceManager;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Centralized flag validation system using SHA-256 hashes.
+ * Provides secure client-side validation and supports progress tracking.
+ */
+public class FlagValidator {
+
+ private static final String TAG = "FlagValidator";
+
+ // Module type constants
+ public static final String TYPE_LESSON = "lesson";
+ public static final String TYPE_CHALLENGE = "challenge";
+
+ // Module identifiers
+ public enum Module {
+ // Reverse Engineering
+ RE_LESSON("re_lesson", TYPE_LESSON),
+ RE_CHALLENGE_1("re_challenge_1", TYPE_CHALLENGE),
+
+ // Insecure Data Storage
+ IDS_LESSON("ids_lesson", TYPE_LESSON),
+ IDS_CHALLENGE_1("ids_challenge_1", TYPE_CHALLENGE),
+
+ // Poor Authentication
+ POOR_AUTH_LESSON("poor_auth_lesson", TYPE_LESSON),
+ POOR_AUTH_CHALLENGE("poor_auth_challenge", TYPE_CHALLENGE),
+
+ // Insecure Authorization
+ INSECURE_AUTH_LESSON("insecure_auth_lesson", TYPE_LESSON),
+
+ // Supply Chain
+ SUPPLY_CHAIN_LESSON("supply_chain_lesson", TYPE_LESSON),
+
+ // Insecure Communication
+ INSECURE_COMM_LESSON("insecure_comm_lesson", TYPE_LESSON),
+ INSECURE_COMM_CHALLENGE("insecure_comm_challenge", TYPE_CHALLENGE),
+
+ // Insufficient Cryptography
+ INSUFFICIENT_CRYPTO_LESSON("insufficient_crypto_lesson", TYPE_LESSON),
+ INSUFFICIENT_CRYPTO_CHALLENGE("insufficient_crypto_challenge", TYPE_CHALLENGE),
+
+ // Security Misconfiguration
+ SECURITY_MISCONFIG_LESSON("security_misconfig_lesson", TYPE_LESSON),
+ SECURITY_MISCONFIG_CHALLENGE_2("security_misconfig_challenge_2", TYPE_CHALLENGE),
+
+ // Input Validation
+ INPUT_VALIDATION_LESSON("input_validation_lesson", TYPE_LESSON),
+
+ // Privacy Controls
+ PRIVACY_LESSON("privacy_lesson", TYPE_LESSON),
+
+ // Client-Side Injection
+ CLIENT_SIDE_INJECTION_LESSON("client_side_injection_lesson", TYPE_LESSON),
+ CLIENT_SIDE_INJECTION_CHALLENGE_1("client_side_injection_challenge_1", TYPE_CHALLENGE),
+ CLIENT_SIDE_INJECTION_CHALLENGE_2("client_side_injection_challenge_2", TYPE_CHALLENGE);
+
+ private final String id;
+ private final String type;
+
+ Module(String id, String type) {
+ this.id = id;
+ this.type = type;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+ // RE_LESSON and RE_CHALLENGE_1 retain local hashes — the SHA-256 in the APK is the
+ // target of the reverse-engineering challenge itself. All other modules require a live
+ // server session; no offline fallback is provided.
+ private static final Map FLAG_HASHES = new HashMap() {{
+ put(Module.RE_LESSON, "a0c066b9cd89c084709330a943fb6b333d45c932ed613e428bc52078b5722e57");
+ put(Module.RE_CHALLENGE_1, "f04a272a2d82a0168f44559f9957dc9e028ecb13195c12fce17ac08d4af91deb");
+ }};
+
+ /**
+ * Validates a flag submission using SHA-256 hash comparison.
+ *
+ * @param module The module being validated
+ * @param submittedFlag The flag submitted by the user
+ * @return true if the flag is correct, false otherwise
+ */
+ public static boolean validateFlag(Module module, String submittedFlag) {
+ if (submittedFlag == null || submittedFlag.trim().isEmpty()) {
+ return false;
+ }
+
+ String expectedHash = FLAG_HASHES.get(module);
+ if (expectedHash == null) {
+ Log.e(TAG, "No hash found for module: " + module.getId());
+ return false;
+ }
+
+ String submittedHash = sha256(submittedFlag.trim());
+ boolean isValid = expectedHash.equalsIgnoreCase(submittedHash);
+
+ Log.d(TAG, isValid ? "[OK] Correct flag for " + module.getId()
+ : "[FAIL] Incorrect flag for " + module.getId());
+ return isValid;
+ }
+
+ /**
+ * Computes SHA-256 hash of the input string.
+ *
+ * @param input The string to hash
+ * @return Hexadecimal representation of the hash
+ */
+ private static String sha256(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+
+ // Convert bytes to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hashBytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "SHA-256 algorithm not available", e);
+ return "";
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Server-side validation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Callback interface for asynchronous flag validation results.
+ *
+ *
{@link #onResult(boolean)} is always invoked on the main (UI) thread.
+ */
+ public interface ValidationCallback {
+ void onResult(boolean correct);
+ }
+
+ /**
+ * Validates a flag against the configured Shepherd server when a session is active.
+ * Falls back to local SHA-256 comparison when no session is available,
+ * so the app remains usable without a running server instance.
+ *
+ *
{@link #onResult(boolean)} is always invoked on the main (UI) thread.
+ */
+ public interface ValidationCallback {
+ void onResult(boolean correct);
+ }
+
+ /**
+ * Validates a flag against the configured Shepherd server when a session is active.
+ * Falls back to local SHA-256 comparison when no session is available,
+ * so the app remains usable without a running server instance.
+ *
+ *
This method is non-blocking. The result is delivered on the main thread via
+ * {@code callback}.
+ *
+ * @param context Application context used to read shared preferences.
+ * @param module The module being validated.
+ * @param flag The flag string submitted by the student.
+ * @param callback Receives {@code true} when the flag is correct, {@code false} otherwise.
+ */
+ public static void validateFlag(
+ Context context,
+ Module module,
+ String flag,
+ ValidationCallback callback) {
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String serverUrl = prefs.getString("server_preference", "").trim();
+ String sessionCookie = AuthManager.getMobileSessionCookie(context);
+
+ if (serverUrl.isEmpty() || sessionCookie.isEmpty()) {
+ // No active server session — only RE modules have offline hashes.
+ Log.d(TAG, "No server session — offline validation for " + module.getId());
+ boolean result = validateFlag(module, flag);
+ new Handler(Looper.getMainLooper()).post(() -> callback.onResult(result));
+ return;
+ }
+
+ final String endpointUrl = serverUrl.replaceAll("/+$", "") + "/mobileFlagSubmit";
+ final String moduleId = module.getId();
+ final String trimmedFlag = flag.trim();
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ new Thread(() -> {
+ boolean correct = false;
+ HttpURLConnection conn = null;
+ try {
+ String body =
+ "moduleId=" + URLEncoder.encode(moduleId, "UTF-8")
+ + "&flag=" + URLEncoder.encode(trimmedFlag, "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpointUrl).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ conn.setRequestProperty("Cookie", sessionCookie);
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ if (status == HttpURLConnection.HTTP_OK) {
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ }
+ JSONObject json = new JSONObject(sb.toString());
+ correct = json.optBoolean("correct", false);
+ Log.d(TAG, "Server validation for " + moduleId + ": " + correct);
+ } else {
+ Log.w(TAG, "Server returned HTTP " + status + " for " + moduleId);
+ correct = validateFlag(module, trimmedFlag);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Server validation failed for " + moduleId + ": " + e.getMessage());
+ correct = validateFlag(module, trimmedFlag);
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+
+ final boolean result = correct;
+ mainHandler.post(() -> callback.onResult(result));
+ }).start();
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/LessonModel.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/LessonModel.java
new file mode 100644
index 000000000..a1215a7cc
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/LessonModel.java
@@ -0,0 +1,50 @@
+package org.owasp.mobileshepherd.ui.lessons;
+
+import android.os.Build;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+public class LessonModel extends ViewModel {
+
+ private final MutableLiveData modelText;
+ private final MutableLiveData serialText;
+ private final MutableLiveData manufacturerText;
+ private final MutableLiveData brandText;
+ private final MutableLiveData sdkText;
+
+ public LessonModel() {
+ serialText = new MutableLiveData<>();
+ modelText = new MutableLiveData<>();
+ manufacturerText = new MutableLiveData<>();
+ brandText = new MutableLiveData<>();
+ sdkText = new MutableLiveData<>();
+
+ // Set device info
+ serialText.setValue(Build.SERIAL);
+ modelText.setValue(Build.MODEL);
+ manufacturerText.setValue(Build.MANUFACTURER);
+ brandText.setValue(Build.BRAND);
+ sdkText.setValue(String.valueOf(Build.VERSION.SDK_INT));
+ }
+
+ public LiveData getSerialText() {
+ return serialText;
+ }
+
+ public LiveData getModelText() {
+ return modelText;
+ }
+
+ public LiveData getManufacturerText() {
+ return manufacturerText;
+ }
+
+ public LiveData getBrandText() {
+ return brandText;
+ }
+
+ public LiveData getSDKText() {
+ return sdkText;
+ }
+}
\ No newline at end of file
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/clientsideinjection/ClientSideInjectionLessonFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/clientsideinjection/ClientSideInjectionLessonFragment.java
new file mode 100644
index 000000000..1b33197a5
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/clientsideinjection/ClientSideInjectionLessonFragment.java
@@ -0,0 +1,274 @@
+package org.owasp.mobileshepherd.ui.lessons.clientsideinjection;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import org.owasp.mobileshepherd.R;
+import org.owasp.mobileshepherd.databinding.FragmentClientSideInjectionLessonBinding;
+import org.owasp.mobileshepherd.ui.lessons.clientsideinjection.helpers.DatabaseHelper;
+import org.owasp.mobileshepherd.utils.AuthManager;
+import org.owasp.mobileshepherd.utils.FlagProvider;
+import org.owasp.mobileshepherd.utils.FlagValidator;
+import org.owasp.mobileshepherd.utils.ModuleInfoHelper;
+import org.owasp.mobileshepherd.utils.ProgressTracker;
+
+public class ClientSideInjectionLessonFragment extends Fragment {
+
+ private FragmentClientSideInjectionLessonBinding binding;
+ private DatabaseHelper dbHelper;
+ private static final String TAG = "ClientSideInjection";
+ private boolean fabExpanded = false;
+ private ProgressTracker progressTracker;
+
+ // The flag seeded into the SQLite DB. In offline mode this is the static
+ // plaintext value; in online mode FlagProvider replaces it with the
+ // server-generated user-specific HMAC after the view is created.
+ private String currentFlag = "";
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ binding = FragmentClientSideInjectionLessonBinding.inflate(inflater, container, false);
+ View root = binding.getRoot();
+
+ // Initialize database
+ dbHelper = new DatabaseHelper(requireContext());
+ progressTracker = new ProgressTracker(requireContext());
+
+ // Seed with offline flag immediately so the lesson is usable right away,
+ // then asynchronously replace with the dynamic server flag if online.
+ FlagProvider.getFlag(
+ requireContext(),
+ FlagValidator.Module.CLIENT_SIDE_INJECTION_LESSON,
+ flag -> {
+ if (!isAdded()) return;
+ currentFlag = flag;
+ initializeDatabase(flag);
+ });
+
+ // Setup expandable FAB with command reference and OWASP link
+ FloatingActionButton fab = requireActivity().findViewById(R.id.fab);
+ FloatingActionButton fabCommandRef = requireActivity().findViewById(R.id.fab_command_reference);
+ FloatingActionButton fabOwaspLink = requireActivity().findViewById(R.id.fab_owasp_link);
+
+ if (fab != null) {
+ fab.setOnClickListener(v -> ModuleInfoHelper.showDialog(requireContext(), FlagValidator.Module.CLIENT_SIDE_INJECTION_LESSON));
+ }
+ if (fabCommandRef != null) {
+ fabCommandRef.setOnClickListener(v -> {
+ showDetailedInfo();
+ collapseFab(fab, fabCommandRef, fabOwaspLink);
+ });
+ }
+
+
+ // Setup search button
+ binding.searchButton.setOnClickListener(v -> performSearch());
+
+ return root;
+ }
+
+ private void initializeDatabase(String flagValue) {
+ SQLiteDatabase db = dbHelper.getWritableDatabase();
+
+ // Clear existing data
+ db.execSQL("DELETE FROM users");
+
+ // Insert sample data
+ insertUser(db, "admin", "admin@app.com", "Administrator", false);
+ insertUser(db, "alice", "alice@app.com", "Alice Smith", false);
+ insertUser(db, "bob", "bob@app.com", "Bob Jones", false);
+ insertUser(db, "charlie", "charlie@app.com", "Charlie Brown", false);
+
+ // Insert hidden admin user with flag (obscure username)
+ insertUser(db, "sys_root", "root@system.internal", flagValue, true);
+
+ db.close();
+ }
+
+ private void insertUser(SQLiteDatabase db, String username, String email, String fullName, boolean isAdmin) {
+ ContentValues values = new ContentValues();
+ values.put("username", username);
+ values.put("email", email);
+ values.put("full_name", fullName);
+ values.put("is_admin", isAdmin ? 1 : 0);
+ db.insert("users", null, values);
+ }
+
+ private void performSearch() {
+ String searchTerm = binding.searchInput.getText().toString();
+
+ if (searchTerm.isEmpty()) {
+ Toast.makeText(getContext(), "Please enter a search term", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // VULNERABLE: Concatenating user input directly into SQL query
+ String query = "SELECT username, email, full_name, is_admin FROM users WHERE username = '" + searchTerm + "'";
+
+ Log.d(TAG, "Executing query: " + query);
+
+ SQLiteDatabase db = dbHelper.getReadableDatabase();
+ Cursor cursor = null;
+
+ try {
+ cursor = db.rawQuery(query, null);
+
+ int count = 0;
+ LinearLayout container = binding.resultContainer;
+ container.removeAllViews();
+
+ while (cursor.moveToNext()) {
+ count++;
+ String username = cursor.getString(0);
+ String email = cursor.getString(1);
+ String fullName = cursor.getString(2);
+ int isAdmin = cursor.getInt(3);
+
+ View row = LayoutInflater.from(requireContext())
+ .inflate(R.layout.item_user_result, container, false);
+ ((TextView) row.findViewById(R.id.item_username)).setText(username);
+ ((TextView) row.findViewById(R.id.item_email)).setText(email);
+ ((TextView) row.findViewById(R.id.item_full_name)).setText(
+ fullName != null ? fullName : "");
+ TextView adminView = row.findViewById(R.id.item_is_admin);
+ if (isAdmin == 1) {
+ adminView.setText("Yes");
+ adminView.setTextColor(getResources().getColor(
+ android.R.color.holo_red_light, null));
+ } else {
+ adminView.setText("No");
+ adminView.setTextColor(getResources().getColor(
+ android.R.color.darker_gray, null));
+ }
+ container.addView(row);
+
+ // Check if this is the hidden flag row
+ if (fullName != null && !currentFlag.isEmpty()
+ && fullName.equals(currentFlag)) {
+ submitFlagToServer(fullName);
+ }
+ }
+
+ if (count == 0) {
+ binding.resultEmptyText.setText("No users found matching: " + searchTerm);
+ binding.resultEmptyText.setVisibility(View.VISIBLE);
+ binding.resultContainer.setVisibility(View.GONE);
+ } else {
+ binding.resultEmptyText.setVisibility(View.GONE);
+ binding.resultContainer.setVisibility(View.VISIBLE);
+ }
+
+ } catch (Exception e) {
+ binding.resultEmptyText.setText("Error: " + e.getMessage() + "\n\nTip: Check your SQL syntax!");
+ binding.resultEmptyText.setVisibility(View.VISIBLE);
+ binding.resultContainer.setVisibility(View.GONE);
+ Log.e(TAG, "SQL Error", e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ db.close();
+ }
+ }
+
+ private void toggleFabExpansion(FloatingActionButton mainFab, FloatingActionButton fab1, FloatingActionButton fab2) {
+ fabExpanded = !fabExpanded;
+ if (fabExpanded) {
+ if (fab1 != null) fab1.setVisibility(View.VISIBLE);
+ if (mainFab != null) mainFab.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
+ } else {
+ collapseFab(mainFab, fab1, fab2);
+ }
+ }
+
+ private void collapseFab(FloatingActionButton mainFab, FloatingActionButton fab1, FloatingActionButton fab2) {
+ fabExpanded = false;
+ if (fab1 != null) fab1.setVisibility(View.GONE);
+ if (fab2 != null) fab2.setVisibility(View.GONE);
+ if (mainFab != null) mainFab.setImageResource(android.R.drawable.ic_menu_help);
+ }
+
+
+ private void showDetailedInfo() {
+ View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_lesson_info, null);
+
+ TextView introText = dialogView.findViewById(R.id.intro_text);
+ // TextView vulnerabilitiesText = dialogView.findViewById(R.id.vulnerabilities_text);
+ View hintsSection = dialogView.findViewById(R.id.hints_section);
+ TextView hintsText = dialogView.findViewById(R.id.hints_text);
+ View additionalSection = dialogView.findViewById(R.id.additional_section);
+
+ introText.setText(R.string.client_side_injection_lesson_intro);
+ // vulnerabilitiesText.setText(R.string.client_side_injection_lesson_vulnerabilities);
+
+ hintsSection.setVisibility(View.GONE);
+ additionalSection.setVisibility(View.GONE);
+
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Client-Side Injection Lesson\nOWASP M7: Client Code Quality")
+ .setView(dialogView)
+ .setPositiveButton("Close", null)
+ .show();
+ }
+
+ /**
+ * Submits the discovered flag to the Shepherd server for validation.
+ * Falls back to local SHA-256 comparison when no server is configured.
+ * Marks the lesson complete and updates the FAB appearance on success.
+ */
+ private void submitFlagToServer(String flag) {
+ // In case the DB was seeded before FlagProvider returned, use the
+ // most recent flag value rather than the one passed by the search results.
+ String flagToSubmit = currentFlag.isEmpty() ? flag : currentFlag;
+ FlagValidator.validateFlag(
+ requireContext(),
+ FlagValidator.Module.CLIENT_SIDE_INJECTION_LESSON,
+ flagToSubmit,
+ correct -> {
+ if (!isAdded()) return;
+ if (correct) {
+ progressTracker.markCompleted(FlagValidator.Module.CLIENT_SIDE_INJECTION_LESSON);
+ new androidx.appcompat.app.AlertDialog.Builder(requireContext())
+ .setTitle("Lesson Complete")
+ .setMessage("Correct flag validated! You have successfully demonstrated "
+ + "a client-side SQL injection attack.")
+ .setPositiveButton("OK", null)
+ .show();
+ } else {
+ Toast.makeText(requireContext(),
+ "Flag incorrect — keep trying!", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ FloatingActionButton fab = requireActivity().findViewById(R.id.fab);
+ FloatingActionButton fabCommandRef = requireActivity().findViewById(R.id.fab_command_reference);
+ FloatingActionButton fabOwaspLink = requireActivity().findViewById(R.id.fab_owasp_link);
+ collapseFab(fab, fabCommandRef, fabOwaspLink);
+ if (dbHelper != null) {
+ dbHelper.close();
+ }
+ binding = null;
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/clientsideinjection/helpers/DatabaseHelper.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/clientsideinjection/helpers/DatabaseHelper.java
new file mode 100644
index 000000000..8ed215e87
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/clientsideinjection/helpers/DatabaseHelper.java
@@ -0,0 +1,32 @@
+package org.owasp.mobileshepherd.ui.lessons.clientsideinjection.helpers;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class DatabaseHelper extends SQLiteOpenHelper {
+
+ private static final String DATABASE_NAME = "client_injection.db";
+ private static final int DATABASE_VERSION = 1;
+
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ String createTable = "CREATE TABLE users (" +
+ "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "username TEXT NOT NULL, " +
+ "email TEXT NOT NULL, " +
+ "full_name TEXT, " +
+ "is_admin INTEGER DEFAULT 0)";
+ db.execSQL(createTable);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL("DROP TABLE IF EXISTS users");
+ onCreate(db);
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/crypto/InsufficientCryptoLessonFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/crypto/InsufficientCryptoLessonFragment.java
new file mode 100644
index 000000000..64624ccd1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/crypto/InsufficientCryptoLessonFragment.java
@@ -0,0 +1,258 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.preference.PreferenceManager;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Centralized flag validation system using SHA-256 hashes.
+ * Provides secure client-side validation and supports progress tracking.
+ */
+public class FlagValidator {
+
+ private static final String TAG = "FlagValidator";
+
+ // Module type constants
+ public static final String TYPE_LESSON = "lesson";
+ public static final String TYPE_CHALLENGE = "challenge";
+
+ // Module identifiers
+ public enum Module {
+ // Reverse Engineering
+ RE_LESSON("re_lesson", TYPE_LESSON),
+ RE_CHALLENGE_1("re_challenge_1", TYPE_CHALLENGE),
+
+ // Insecure Data Storage
+ IDS_LESSON("ids_lesson", TYPE_LESSON),
+ IDS_CHALLENGE_1("ids_challenge_1", TYPE_CHALLENGE),
+
+ // Poor Authentication
+ POOR_AUTH_LESSON("poor_auth_lesson", TYPE_LESSON),
+ POOR_AUTH_CHALLENGE("poor_auth_challenge", TYPE_CHALLENGE),
+
+ // Insecure Authorization
+ INSECURE_AUTH_LESSON("insecure_auth_lesson", TYPE_LESSON),
+
+ // Supply Chain
+ SUPPLY_CHAIN_LESSON("supply_chain_lesson", TYPE_LESSON),
+
+ // Insecure Communication
+ INSECURE_COMM_LESSON("insecure_comm_lesson", TYPE_LESSON),
+ INSECURE_COMM_CHALLENGE("insecure_comm_challenge", TYPE_CHALLENGE),
+
+ // Insufficient Cryptography
+ INSUFFICIENT_CRYPTO_LESSON("insufficient_crypto_lesson", TYPE_LESSON),
+ INSUFFICIENT_CRYPTO_CHALLENGE("insufficient_crypto_challenge", TYPE_CHALLENGE),
+
+ // Security Misconfiguration
+ SECURITY_MISCONFIG_LESSON("security_misconfig_lesson", TYPE_LESSON),
+ SECURITY_MISCONFIG_CHALLENGE_2("security_misconfig_challenge_2", TYPE_CHALLENGE),
+
+ // Input Validation
+ INPUT_VALIDATION_LESSON("input_validation_lesson", TYPE_LESSON),
+
+ // Privacy Controls
+ PRIVACY_LESSON("privacy_lesson", TYPE_LESSON),
+
+ // Client-Side Injection
+ CLIENT_SIDE_INJECTION_LESSON("client_side_injection_lesson", TYPE_LESSON),
+ CLIENT_SIDE_INJECTION_CHALLENGE_1("client_side_injection_challenge_1", TYPE_CHALLENGE),
+ CLIENT_SIDE_INJECTION_CHALLENGE_2("client_side_injection_challenge_2", TYPE_CHALLENGE);
+
+ private final String id;
+ private final String type;
+
+ Module(String id, String type) {
+ this.id = id;
+ this.type = type;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+ // RE_LESSON and RE_CHALLENGE_1 retain local hashes — the SHA-256 in the APK is the
+ // target of the reverse-engineering challenge itself. All other modules require a live
+ // server session; no offline fallback is provided.
+ private static final Map FLAG_HASHES = new HashMap() {{
+ put(Module.RE_LESSON, "a0c066b9cd89c084709330a943fb6b333d45c932ed613e428bc52078b5722e57");
+ put(Module.RE_CHALLENGE_1, "f04a272a2d82a0168f44559f9957dc9e028ecb13195c12fce17ac08d4af91deb");
+ }};
+
+ /**
+ * Validates a flag submission using SHA-256 hash comparison.
+ *
+ * @param module The module being validated
+ * @param submittedFlag The flag submitted by the user
+ * @return true if the flag is correct, false otherwise
+ */
+ public static boolean validateFlag(Module module, String submittedFlag) {
+ if (submittedFlag == null || submittedFlag.trim().isEmpty()) {
+ return false;
+ }
+
+ String expectedHash = FLAG_HASHES.get(module);
+ if (expectedHash == null) {
+ Log.e(TAG, "No hash found for module: " + module.getId());
+ return false;
+ }
+
+ String submittedHash = sha256(submittedFlag.trim());
+ boolean isValid = expectedHash.equalsIgnoreCase(submittedHash);
+
+ Log.d(TAG, isValid ? "[OK] Correct flag for " + module.getId()
+ : "[FAIL] Incorrect flag for " + module.getId());
+ return isValid;
+ }
+
+ /**
+ * Computes SHA-256 hash of the input string.
+ *
+ * @param input The string to hash
+ * @return Hexadecimal representation of the hash
+ */
+ private static String sha256(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+
+ // Convert bytes to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hashBytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "SHA-256 algorithm not available", e);
+ return "";
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Server-side validation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Callback interface for asynchronous flag validation results.
+ *
+ *
{@link #onResult(boolean)} is always invoked on the main (UI) thread.
+ */
+ public interface ValidationCallback {
+ void onResult(boolean correct);
+ }
+
+ /**
+ * Validates a flag against the configured Shepherd server when a session is active.
+ * Falls back to local SHA-256 comparison when no session is available,
+ * so the app remains usable without a running server instance.
+ *
+ *
This method is non-blocking. The result is delivered on the main thread via
+ * {@code callback}.
+ *
+ * @param context Application context used to read shared preferences.
+ * @param module The module being validated.
+ * @param flag The flag string submitted by the student.
+ * @param callback Receives {@code true} when the flag is correct, {@code false} otherwise.
+ */
+ public static void validateFlag(
+ Context context,
+ Module module,
+ String flag,
+ ValidationCallback callback) {
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String serverUrl = prefs.getString("server_preference", "").trim();
+ String sessionCookie = AuthManager.getMobileSessionCookie(context);
+
+ if (serverUrl.isEmpty() || sessionCookie.isEmpty()) {
+ // No active server session — only RE modules have offline hashes.
+ Log.d(TAG, "No server session — offline validation for " + module.getId());
+ boolean result = validateFlag(module, flag);
+ new Handler(Looper.getMainLooper()).post(() -> callback.onResult(result));
+ return;
+ }
+
+ final String endpointUrl = serverUrl.replaceAll("/+$", "") + "/mobileFlagSubmit";
+ final String moduleId = module.getId();
+ final String trimmedFlag = flag.trim();
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ new Thread(() -> {
+ boolean correct = false;
+ HttpURLConnection conn = null;
+ try {
+ String body =
+ "moduleId=" + URLEncoder.encode(moduleId, "UTF-8")
+ + "&flag=" + URLEncoder.encode(trimmedFlag, "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpointUrl).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ conn.setRequestProperty("Cookie", sessionCookie);
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ if (status == HttpURLConnection.HTTP_OK) {
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ }
+ JSONObject json = new JSONObject(sb.toString());
+ correct = json.optBoolean("correct", false);
+ Log.d(TAG, "Server validation for " + moduleId + ": " + correct);
+ } else {
+ Log.w(TAG, "Server returned HTTP " + status + " for " + moduleId);
+ correct = validateFlag(module, trimmedFlag);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Server validation failed for " + moduleId + ": " + e.getMessage());
+ correct = validateFlag(module, trimmedFlag);
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+
+ final boolean result = correct;
+ mainHandler.post(() -> callback.onResult(result));
+ }).start();
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecureauthorization/InsecureAuthorizationLessonFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecureauthorization/InsecureAuthorizationLessonFragment.java
new file mode 100644
index 000000000..f6b8a7b24
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecureauthorization/InsecureAuthorizationLessonFragment.java
@@ -0,0 +1,241 @@
+package org.owasp.mobileshepherd.ui.lessons.insecureauthorization;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import org.owasp.mobileshepherd.R;
+import org.owasp.mobileshepherd.databinding.FragmentInsecureAuthorizationLessonBinding;
+import org.owasp.mobileshepherd.utils.FlagProvider;
+import org.owasp.mobileshepherd.utils.FlagValidator;
+import org.owasp.mobileshepherd.utils.ModuleInfoHelper;
+import org.owasp.mobileshepherd.utils.ProgressTracker;
+
+public class InsecureAuthorizationLessonFragment extends Fragment {
+
+ private FragmentInsecureAuthorizationLessonBinding binding;
+ private SharedPreferences prefs;
+ private static final String PREFS_NAME = "UserSession";
+ private boolean fabExpanded = false;
+ private ProgressTracker progressTracker;
+ private String currentFlag = "";
+
+ // Demo credentials
+ private static final String DEMO_USERNAME = "testuser";
+ private static final String DEMO_PASSWORD = "password123";
+
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+ binding = FragmentInsecureAuthorizationLessonBinding.inflate(inflater, container, false);
+ View root = binding.getRoot();
+
+ prefs = requireContext().getSharedPreferences(PREFS_NAME, android.content.Context.MODE_PRIVATE);
+ progressTracker = new ProgressTracker(requireContext());
+ FlagProvider.getFlag(
+ requireContext(),
+ FlagValidator.Module.INSECURE_AUTH_LESSON,
+ flagValue -> currentFlag = flagValue);
+
+ // Setup expandable FAB with command reference and OWASP link
+ FloatingActionButton fab = requireActivity().findViewById(R.id.fab);
+ FloatingActionButton fabCommandRef = requireActivity().findViewById(R.id.fab_command_reference);
+ FloatingActionButton fabOwaspLink = requireActivity().findViewById(R.id.fab_owasp_link);
+
+ if (fab != null) {
+ fab.setOnClickListener(v -> ModuleInfoHelper.showDialog(requireContext(), FlagValidator.Module.INSECURE_AUTH_LESSON));
+ }
+ if (fabCommandRef != null) {
+ fabCommandRef.setOnClickListener(v -> {
+ showDetailedInfo();
+ collapseFab(fab, fabCommandRef, fabOwaspLink);
+ });
+ }
+
+
+ // Check if already logged in
+ if (isLoggedIn()) {
+ showDashboard();
+ } else {
+ showLoginForm();
+ }
+
+ // Log the vulnerability hint
+ Log.d("AuthVulnerability", "Authorization check uses client-side role from SharedPreferences");
+ Log.d("AuthVulnerability", "Role key: 'user_role' - Values: 'user' or 'admin'");
+
+ return root;
+ }
+
+ private void showLoginForm() {
+ binding.loginCard.setVisibility(View.VISIBLE);
+ binding.dashboardCard.setVisibility(View.GONE);
+
+ Button loginButton = binding.loginButton;
+ EditText usernameInput = binding.usernameInput;
+ EditText passwordInput = binding.passwordInput;
+
+ loginButton.setOnClickListener(v -> {
+ String username = usernameInput.getText().toString().trim();
+ String password = passwordInput.getText().toString().trim();
+
+ if (username.equals(DEMO_USERNAME) && password.equals(DEMO_PASSWORD)) {
+ // Successful login - store session with basic user role
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean("is_logged_in", true);
+ editor.putString("username", username);
+ editor.putString("user_role", "user"); // Insecure: stored client-side!
+ editor.putLong("login_timestamp", System.currentTimeMillis());
+ editor.apply();
+
+ Log.i("Authorization", "User logged in: " + username + " with role: user");
+ Log.d("Authorization", "Session stored in SharedPreferences: " +
+ requireContext().getApplicationInfo().dataDir + "/shared_prefs/UserSession.xml");
+
+ Toast.makeText(getContext(), "Login successful!", Toast.LENGTH_SHORT).show();
+ showDashboard();
+ } else {
+ Toast.makeText(getContext(), "Invalid credentials. Try: testuser / password123", Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ private void showDashboard() {
+ binding.loginCard.setVisibility(View.GONE);
+ binding.dashboardCard.setVisibility(View.VISIBLE);
+
+ String username = prefs.getString("username", "User");
+ String role = prefs.getString("user_role", "user");
+
+ binding.welcomeText.setText("Welcome, " + username + "!");
+ binding.roleText.setText("Current Role: " + role);
+
+ Log.d("Authorization", "Dashboard loaded for user: " + username + " (role: " + role + ")");
+
+ // Access Admin Panel button
+ binding.adminPanelButton.setOnClickListener(v -> {
+ accessAdminPanel();
+ });
+
+ // Logout button
+ binding.logoutButton.setOnClickListener(v -> {
+ logout();
+ });
+ }
+
+ private void accessAdminPanel() {
+ String role = prefs.getString("user_role", "user");
+
+ Log.d("Authorization", "Admin panel access attempt - Current role: " + role);
+
+ // INSECURE: Authorization check relies on client-controlled value
+ if ("admin".equals(role)) {
+ // Admin access granted
+ String flag = currentFlag;
+ binding.adminContentCard.setVisibility(View.VISIBLE);
+ binding.flagText.setText("Admin Flag: " + flag);
+ binding.accessDeniedText.setVisibility(View.GONE);
+
+ Toast.makeText(getContext(), "Admin access granted! Flag revealed!", Toast.LENGTH_LONG).show();
+ Log.i("Authorization", "ADMIN ACCESS GRANTED - Flag revealed");
+
+ FlagValidator.validateFlag(requireContext(), FlagValidator.Module.INSECURE_AUTH_LESSON,
+ flag, correct -> {
+ if (!isAdded()) return;
+ if (correct) {
+ progressTracker.markCompleted(FlagValidator.Module.INSECURE_AUTH_LESSON);
+ }
+ });
+ } else {
+ // Access denied
+ binding.adminContentCard.setVisibility(View.VISIBLE);
+ binding.flagText.setText("");
+ binding.accessDeniedText.setVisibility(View.VISIBLE);
+
+ Toast.makeText(getContext(), "Access Denied: Admin privileges required", Toast.LENGTH_SHORT).show();
+ Log.w("Authorization", "Admin access DENIED for role: " + role);
+ }
+ }
+
+ private void toggleFabExpansion(FloatingActionButton mainFab, FloatingActionButton fab1, FloatingActionButton fab2) {
+ fabExpanded = !fabExpanded;
+ if (fabExpanded) {
+ if (fab1 != null) fab1.setVisibility(View.VISIBLE);
+ if (mainFab != null) mainFab.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
+ } else {
+ collapseFab(mainFab, fab1, fab2);
+ }
+ }
+
+ private void collapseFab(FloatingActionButton mainFab, FloatingActionButton fab1, FloatingActionButton fab2) {
+ fabExpanded = false;
+ if (fab1 != null) fab1.setVisibility(View.GONE);
+ if (fab2 != null) fab2.setVisibility(View.GONE);
+ if (mainFab != null) mainFab.setImageResource(android.R.drawable.ic_menu_help);
+ }
+
+
+ private void showDetailedInfo() {
+ View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_lesson_info, null);
+
+ TextView introText = dialogView.findViewById(R.id.intro_text);
+ // TextView vulnerabilitiesText = dialogView.findViewById(R.id.vulnerabilities_text);
+ View hintsSection = dialogView.findViewById(R.id.hints_section);
+ TextView hintsText = dialogView.findViewById(R.id.hints_text);
+ // View bestPracticesSection = dialogView.findViewById(R.id.best_practices_section);
+ View additionalSection = dialogView.findViewById(R.id.additional_section);
+
+ introText.setText(R.string.insecure_authorization_intro);
+ // vulnerabilitiesText.setText("• Client-side authorization stored in SharedPreferences\n• Privilege escalation by modifying local role data\n• No server-side validation of permissions");
+
+ hintsSection.setVisibility(View.GONE);
+
+ // bestPracticesSection.setVisibility(View.GONE);
+ additionalSection.setVisibility(View.GONE);
+
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Insecure Authorization\nOWASP M3: Insecure Authentication/Authorization")
+ .setView(dialogView)
+ .setPositiveButton("Close", null)
+ .show();
+ }
+
+ private boolean isLoggedIn() {
+ return prefs.getBoolean("is_logged_in", false);
+ }
+
+ private void logout() {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.clear();
+ editor.apply();
+
+ Toast.makeText(getContext(), "Logged out successfully", Toast.LENGTH_SHORT).show();
+ showLoginForm();
+ binding.adminContentCard.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ FloatingActionButton fab = requireActivity().findViewById(R.id.fab);
+ FloatingActionButton fabCommandRef = requireActivity().findViewById(R.id.fab_command_reference);
+ FloatingActionButton fabOwaspLink = requireActivity().findViewById(R.id.fab_owasp_link);
+ collapseFab(fab, fabCommandRef, fabOwaspLink);
+ binding = null;
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecurecomm/InsecureCommLessonFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecurecomm/InsecureCommLessonFragment.java
new file mode 100644
index 000000000..64624ccd1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecurecomm/InsecureCommLessonFragment.java
@@ -0,0 +1,258 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.preference.PreferenceManager;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Centralized flag validation system using SHA-256 hashes.
+ * Provides secure client-side validation and supports progress tracking.
+ */
+public class FlagValidator {
+
+ private static final String TAG = "FlagValidator";
+
+ // Module type constants
+ public static final String TYPE_LESSON = "lesson";
+ public static final String TYPE_CHALLENGE = "challenge";
+
+ // Module identifiers
+ public enum Module {
+ // Reverse Engineering
+ RE_LESSON("re_lesson", TYPE_LESSON),
+ RE_CHALLENGE_1("re_challenge_1", TYPE_CHALLENGE),
+
+ // Insecure Data Storage
+ IDS_LESSON("ids_lesson", TYPE_LESSON),
+ IDS_CHALLENGE_1("ids_challenge_1", TYPE_CHALLENGE),
+
+ // Poor Authentication
+ POOR_AUTH_LESSON("poor_auth_lesson", TYPE_LESSON),
+ POOR_AUTH_CHALLENGE("poor_auth_challenge", TYPE_CHALLENGE),
+
+ // Insecure Authorization
+ INSECURE_AUTH_LESSON("insecure_auth_lesson", TYPE_LESSON),
+
+ // Supply Chain
+ SUPPLY_CHAIN_LESSON("supply_chain_lesson", TYPE_LESSON),
+
+ // Insecure Communication
+ INSECURE_COMM_LESSON("insecure_comm_lesson", TYPE_LESSON),
+ INSECURE_COMM_CHALLENGE("insecure_comm_challenge", TYPE_CHALLENGE),
+
+ // Insufficient Cryptography
+ INSUFFICIENT_CRYPTO_LESSON("insufficient_crypto_lesson", TYPE_LESSON),
+ INSUFFICIENT_CRYPTO_CHALLENGE("insufficient_crypto_challenge", TYPE_CHALLENGE),
+
+ // Security Misconfiguration
+ SECURITY_MISCONFIG_LESSON("security_misconfig_lesson", TYPE_LESSON),
+ SECURITY_MISCONFIG_CHALLENGE_2("security_misconfig_challenge_2", TYPE_CHALLENGE),
+
+ // Input Validation
+ INPUT_VALIDATION_LESSON("input_validation_lesson", TYPE_LESSON),
+
+ // Privacy Controls
+ PRIVACY_LESSON("privacy_lesson", TYPE_LESSON),
+
+ // Client-Side Injection
+ CLIENT_SIDE_INJECTION_LESSON("client_side_injection_lesson", TYPE_LESSON),
+ CLIENT_SIDE_INJECTION_CHALLENGE_1("client_side_injection_challenge_1", TYPE_CHALLENGE),
+ CLIENT_SIDE_INJECTION_CHALLENGE_2("client_side_injection_challenge_2", TYPE_CHALLENGE);
+
+ private final String id;
+ private final String type;
+
+ Module(String id, String type) {
+ this.id = id;
+ this.type = type;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+ // RE_LESSON and RE_CHALLENGE_1 retain local hashes — the SHA-256 in the APK is the
+ // target of the reverse-engineering challenge itself. All other modules require a live
+ // server session; no offline fallback is provided.
+ private static final Map FLAG_HASHES = new HashMap() {{
+ put(Module.RE_LESSON, "a0c066b9cd89c084709330a943fb6b333d45c932ed613e428bc52078b5722e57");
+ put(Module.RE_CHALLENGE_1, "f04a272a2d82a0168f44559f9957dc9e028ecb13195c12fce17ac08d4af91deb");
+ }};
+
+ /**
+ * Validates a flag submission using SHA-256 hash comparison.
+ *
+ * @param module The module being validated
+ * @param submittedFlag The flag submitted by the user
+ * @return true if the flag is correct, false otherwise
+ */
+ public static boolean validateFlag(Module module, String submittedFlag) {
+ if (submittedFlag == null || submittedFlag.trim().isEmpty()) {
+ return false;
+ }
+
+ String expectedHash = FLAG_HASHES.get(module);
+ if (expectedHash == null) {
+ Log.e(TAG, "No hash found for module: " + module.getId());
+ return false;
+ }
+
+ String submittedHash = sha256(submittedFlag.trim());
+ boolean isValid = expectedHash.equalsIgnoreCase(submittedHash);
+
+ Log.d(TAG, isValid ? "[OK] Correct flag for " + module.getId()
+ : "[FAIL] Incorrect flag for " + module.getId());
+ return isValid;
+ }
+
+ /**
+ * Computes SHA-256 hash of the input string.
+ *
+ * @param input The string to hash
+ * @return Hexadecimal representation of the hash
+ */
+ private static String sha256(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+
+ // Convert bytes to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hashBytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "SHA-256 algorithm not available", e);
+ return "";
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Server-side validation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Callback interface for asynchronous flag validation results.
+ *
+ *
{@link #onResult(boolean)} is always invoked on the main (UI) thread.
+ */
+ public interface ValidationCallback {
+ void onResult(boolean correct);
+ }
+
+ /**
+ * Validates a flag against the configured Shepherd server when a session is active.
+ * Falls back to local SHA-256 comparison when no session is available,
+ * so the app remains usable without a running server instance.
+ *
+ *
This method is non-blocking. The result is delivered on the main thread via
+ * {@code callback}.
+ *
+ * @param context Application context used to read shared preferences.
+ * @param module The module being validated.
+ * @param flag The flag string submitted by the student.
+ * @param callback Receives {@code true} when the flag is correct, {@code false} otherwise.
+ */
+ public static void validateFlag(
+ Context context,
+ Module module,
+ String flag,
+ ValidationCallback callback) {
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String serverUrl = prefs.getString("server_preference", "").trim();
+ String sessionCookie = AuthManager.getMobileSessionCookie(context);
+
+ if (serverUrl.isEmpty() || sessionCookie.isEmpty()) {
+ // No active server session — only RE modules have offline hashes.
+ Log.d(TAG, "No server session — offline validation for " + module.getId());
+ boolean result = validateFlag(module, flag);
+ new Handler(Looper.getMainLooper()).post(() -> callback.onResult(result));
+ return;
+ }
+
+ final String endpointUrl = serverUrl.replaceAll("/+$", "") + "/mobileFlagSubmit";
+ final String moduleId = module.getId();
+ final String trimmedFlag = flag.trim();
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ new Thread(() -> {
+ boolean correct = false;
+ HttpURLConnection conn = null;
+ try {
+ String body =
+ "moduleId=" + URLEncoder.encode(moduleId, "UTF-8")
+ + "&flag=" + URLEncoder.encode(trimmedFlag, "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpointUrl).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ conn.setRequestProperty("Cookie", sessionCookie);
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ if (status == HttpURLConnection.HTTP_OK) {
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ }
+ JSONObject json = new JSONObject(sb.toString());
+ correct = json.optBoolean("correct", false);
+ Log.d(TAG, "Server validation for " + moduleId + ": " + correct);
+ } else {
+ Log.w(TAG, "Server returned HTTP " + status + " for " + moduleId);
+ correct = validateFlag(module, trimmedFlag);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Server validation failed for " + moduleId + ": " + e.getMessage());
+ correct = validateFlag(module, trimmedFlag);
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+
+ final boolean result = correct;
+ mainHandler.post(() -> callback.onResult(result));
+ }).start();
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecuredata/DataStorageDebugActivity.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecuredata/DataStorageDebugActivity.java
new file mode 100644
index 000000000..3c6de0432
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecuredata/DataStorageDebugActivity.java
@@ -0,0 +1,36 @@
+package org.owasp.mobileshepherd.ui.lessons.insecuredata;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import org.owasp.mobileshepherd.R;
+
+public class DataStorageDebugActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Create simple layout
+ TextView textView = new TextView(this);
+ textView.setPadding(48, 48, 48, 48);
+ textView.setTextSize(14);
+
+ // Read some stored data as demonstration
+ SharedPreferences prefs = getSharedPreferences("app_data", MODE_PRIVATE);
+ String userId = prefs.getString("user_id", "demo_user_12345");
+ String apiToken = prefs.getString("api_token", "demo_token_abc123xyz789");
+
+ String message = " Data Storage Debug Panel\n\n" +
+ "[OK] This activity is SAFE (exported=\"false\")\n\n" +
+ "This internal debugging tool shows stored application data:\n\n" +
+ "User ID: " + userId + "\n" +
+ "API Token: " + apiToken + "\n\n" +
+ "Since this activity is NOT exported, only the app itself can access it.\n\n" +
+ "Learn more in the Insecure Data Storage lesson.";
+
+ textView.setText(message);
+ setContentView(textView);
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecuredata/InsecureDataLessonFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecuredata/InsecureDataLessonFragment.java
new file mode 100644
index 000000000..64624ccd1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/insecuredata/InsecureDataLessonFragment.java
@@ -0,0 +1,258 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.preference.PreferenceManager;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Centralized flag validation system using SHA-256 hashes.
+ * Provides secure client-side validation and supports progress tracking.
+ */
+public class FlagValidator {
+
+ private static final String TAG = "FlagValidator";
+
+ // Module type constants
+ public static final String TYPE_LESSON = "lesson";
+ public static final String TYPE_CHALLENGE = "challenge";
+
+ // Module identifiers
+ public enum Module {
+ // Reverse Engineering
+ RE_LESSON("re_lesson", TYPE_LESSON),
+ RE_CHALLENGE_1("re_challenge_1", TYPE_CHALLENGE),
+
+ // Insecure Data Storage
+ IDS_LESSON("ids_lesson", TYPE_LESSON),
+ IDS_CHALLENGE_1("ids_challenge_1", TYPE_CHALLENGE),
+
+ // Poor Authentication
+ POOR_AUTH_LESSON("poor_auth_lesson", TYPE_LESSON),
+ POOR_AUTH_CHALLENGE("poor_auth_challenge", TYPE_CHALLENGE),
+
+ // Insecure Authorization
+ INSECURE_AUTH_LESSON("insecure_auth_lesson", TYPE_LESSON),
+
+ // Supply Chain
+ SUPPLY_CHAIN_LESSON("supply_chain_lesson", TYPE_LESSON),
+
+ // Insecure Communication
+ INSECURE_COMM_LESSON("insecure_comm_lesson", TYPE_LESSON),
+ INSECURE_COMM_CHALLENGE("insecure_comm_challenge", TYPE_CHALLENGE),
+
+ // Insufficient Cryptography
+ INSUFFICIENT_CRYPTO_LESSON("insufficient_crypto_lesson", TYPE_LESSON),
+ INSUFFICIENT_CRYPTO_CHALLENGE("insufficient_crypto_challenge", TYPE_CHALLENGE),
+
+ // Security Misconfiguration
+ SECURITY_MISCONFIG_LESSON("security_misconfig_lesson", TYPE_LESSON),
+ SECURITY_MISCONFIG_CHALLENGE_2("security_misconfig_challenge_2", TYPE_CHALLENGE),
+
+ // Input Validation
+ INPUT_VALIDATION_LESSON("input_validation_lesson", TYPE_LESSON),
+
+ // Privacy Controls
+ PRIVACY_LESSON("privacy_lesson", TYPE_LESSON),
+
+ // Client-Side Injection
+ CLIENT_SIDE_INJECTION_LESSON("client_side_injection_lesson", TYPE_LESSON),
+ CLIENT_SIDE_INJECTION_CHALLENGE_1("client_side_injection_challenge_1", TYPE_CHALLENGE),
+ CLIENT_SIDE_INJECTION_CHALLENGE_2("client_side_injection_challenge_2", TYPE_CHALLENGE);
+
+ private final String id;
+ private final String type;
+
+ Module(String id, String type) {
+ this.id = id;
+ this.type = type;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+ // RE_LESSON and RE_CHALLENGE_1 retain local hashes — the SHA-256 in the APK is the
+ // target of the reverse-engineering challenge itself. All other modules require a live
+ // server session; no offline fallback is provided.
+ private static final Map FLAG_HASHES = new HashMap() {{
+ put(Module.RE_LESSON, "a0c066b9cd89c084709330a943fb6b333d45c932ed613e428bc52078b5722e57");
+ put(Module.RE_CHALLENGE_1, "f04a272a2d82a0168f44559f9957dc9e028ecb13195c12fce17ac08d4af91deb");
+ }};
+
+ /**
+ * Validates a flag submission using SHA-256 hash comparison.
+ *
+ * @param module The module being validated
+ * @param submittedFlag The flag submitted by the user
+ * @return true if the flag is correct, false otherwise
+ */
+ public static boolean validateFlag(Module module, String submittedFlag) {
+ if (submittedFlag == null || submittedFlag.trim().isEmpty()) {
+ return false;
+ }
+
+ String expectedHash = FLAG_HASHES.get(module);
+ if (expectedHash == null) {
+ Log.e(TAG, "No hash found for module: " + module.getId());
+ return false;
+ }
+
+ String submittedHash = sha256(submittedFlag.trim());
+ boolean isValid = expectedHash.equalsIgnoreCase(submittedHash);
+
+ Log.d(TAG, isValid ? "[OK] Correct flag for " + module.getId()
+ : "[FAIL] Incorrect flag for " + module.getId());
+ return isValid;
+ }
+
+ /**
+ * Computes SHA-256 hash of the input string.
+ *
+ * @param input The string to hash
+ * @return Hexadecimal representation of the hash
+ */
+ private static String sha256(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+
+ // Convert bytes to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hashBytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "SHA-256 algorithm not available", e);
+ return "";
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Server-side validation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Callback interface for asynchronous flag validation results.
+ *
+ *
{@link #onResult(boolean)} is always invoked on the main (UI) thread.
+ */
+ public interface ValidationCallback {
+ void onResult(boolean correct);
+ }
+
+ /**
+ * Validates a flag against the configured Shepherd server when a session is active.
+ * Falls back to local SHA-256 comparison when no session is available,
+ * so the app remains usable without a running server instance.
+ *
+ *
This method is non-blocking. The result is delivered on the main thread via
+ * {@code callback}.
+ *
+ * @param context Application context used to read shared preferences.
+ * @param module The module being validated.
+ * @param flag The flag string submitted by the student.
+ * @param callback Receives {@code true} when the flag is correct, {@code false} otherwise.
+ */
+ public static void validateFlag(
+ Context context,
+ Module module,
+ String flag,
+ ValidationCallback callback) {
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String serverUrl = prefs.getString("server_preference", "").trim();
+ String sessionCookie = AuthManager.getMobileSessionCookie(context);
+
+ if (serverUrl.isEmpty() || sessionCookie.isEmpty()) {
+ // No active server session — only RE modules have offline hashes.
+ Log.d(TAG, "No server session — offline validation for " + module.getId());
+ boolean result = validateFlag(module, flag);
+ new Handler(Looper.getMainLooper()).post(() -> callback.onResult(result));
+ return;
+ }
+
+ final String endpointUrl = serverUrl.replaceAll("/+$", "") + "/mobileFlagSubmit";
+ final String moduleId = module.getId();
+ final String trimmedFlag = flag.trim();
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ new Thread(() -> {
+ boolean correct = false;
+ HttpURLConnection conn = null;
+ try {
+ String body =
+ "moduleId=" + URLEncoder.encode(moduleId, "UTF-8")
+ + "&flag=" + URLEncoder.encode(trimmedFlag, "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpointUrl).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ conn.setRequestProperty("Cookie", sessionCookie);
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ if (status == HttpURLConnection.HTTP_OK) {
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ }
+ JSONObject json = new JSONObject(sb.toString());
+ correct = json.optBoolean("correct", false);
+ Log.d(TAG, "Server validation for " + moduleId + ": " + correct);
+ } else {
+ Log.w(TAG, "Server returned HTTP " + status + " for " + moduleId);
+ correct = validateFlag(module, trimmedFlag);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Server validation failed for " + moduleId + ": " + e.getMessage());
+ correct = validateFlag(module, trimmedFlag);
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+
+ final boolean result = correct;
+ mainHandler.post(() -> callback.onResult(result));
+ }).start();
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/poorauth/PoorAuthLessonFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/poorauth/PoorAuthLessonFragment.java
new file mode 100644
index 000000000..ba6a8e144
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/poorauth/PoorAuthLessonFragment.java
@@ -0,0 +1,239 @@
+package org.owasp.mobileshepherd.ui.lessons.poorauth;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import org.owasp.mobileshepherd.R;
+import org.owasp.mobileshepherd.databinding.FragmentPoorAuthLessonBinding;
+import org.owasp.mobileshepherd.utils.FlagProvider;
+import org.owasp.mobileshepherd.utils.FlagValidator;
+import org.owasp.mobileshepherd.utils.ModuleInfoHelper;
+import org.owasp.mobileshepherd.utils.ProgressTracker;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+public class PoorAuthLessonFragment extends Fragment {
+
+ private FragmentPoorAuthLessonBinding binding;
+ private static final String TAG = "PoorAuthLesson";
+ // SHA-256 hash of "654321"
+ private static final String HARDCODED_PIN_HASH = "481f6cc0511143ccdd7e2d1b1b94faf0a700a8b49cd13922a70b5ae28acaa8c5";
+
+ // Obfuscated flag - "Taco_Snores_On_A_Couch"
+ // XOR encoded with key 0x42, then Base64 encoded, then split
+ private static final String[] F = {
+ "FiMhLR", "0RLC0w", "JzEdDS", "wdAx",
+ "0BLTch", "Kg", "=="
+ };
+ private static final byte K = 0x42; // XOR key
+
+ private int attemptCount = 0;
+ private boolean fabExpanded = false;
+ private ProgressTracker progressTracker;
+ private String currentFlag = "";
+
+ public View onCreateView(@NonNull LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+
+ binding = FragmentPoorAuthLessonBinding.inflate(inflater, container, false);
+ View root = binding.getRoot();
+
+ progressTracker = new ProgressTracker(requireContext());
+ FlagProvider.getFlag(
+ requireContext(),
+ FlagValidator.Module.POOR_AUTH_LESSON,
+ flagValue -> currentFlag = flagValue);
+
+ // Setup FAB expansion
+ FloatingActionButton fab = requireActivity().findViewById(R.id.fab);
+ FloatingActionButton fabCommandRef = requireActivity().findViewById(R.id.fab_command_reference);
+ FloatingActionButton fabOwaspLink = requireActivity().findViewById(R.id.fab_owasp_link);
+
+ if (fab != null) {
+ fab.setOnClickListener(v -> ModuleInfoHelper.showDialog(requireContext(), FlagValidator.Module.POOR_AUTH_LESSON));
+ }
+
+ if (fabCommandRef != null) {
+ fabCommandRef.setOnClickListener(v -> {
+ showDetailedInfo();
+ collapseFab(fab, fabCommandRef, fabOwaspLink);
+ });
+ }
+
+
+
+
+ // Set initial FAB appearance based on completion status
+
+ // Log the hardcoded PIN hash (intentionally insecure for demonstration)
+ Log.d(TAG, "Initializing authentication system...");
+ Log.d(TAG, "PIN hash configured: " + HARDCODED_PIN_HASH);
+ Log.d(TAG, "System ready. PIN verification enabled.");
+
+ binding.verifyButton.setOnClickListener(v -> verifyPin());
+
+ return root;
+ }
+
+ private void verifyPin() {
+ String enteredPin = binding.pinInput.getText().toString().trim();
+ attemptCount++;
+
+ if (enteredPin.isEmpty()) {
+ Toast.makeText(getContext(), "Please enter a PIN", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String enteredPinHash = hashPin(enteredPin);
+
+ Log.d(TAG, "PIN verification attempt #" + attemptCount);
+ Log.d(TAG, "Entered PIN hash: " + enteredPinHash);
+ Log.d(TAG, "Expected PIN hash: " + HARDCODED_PIN_HASH);
+
+ if (enteredPinHash != null && enteredPinHash.equals(HARDCODED_PIN_HASH)) {
+ // Successful authentication
+ String flag = currentFlag.isEmpty() ? d() : currentFlag;
+
+ binding.demoCard.setCardBackgroundColor(ContextCompat.getColor(requireContext(), R.color.success_bg));
+ binding.flagText.setText("\u2713 Authentication Successful!\n\nFlag: " + flag);
+ binding.flagText.setVisibility(View.VISIBLE);
+
+ Toast.makeText(getContext(), "Access Granted! Flag revealed!", Toast.LENGTH_LONG).show();
+ Log.d(TAG, "Authentication successful! Flag: " + flag);
+
+ progressTracker.markCompleted(FlagValidator.Module.POOR_AUTH_LESSON);
+ FlagValidator.validateFlag(requireContext(), FlagValidator.Module.POOR_AUTH_LESSON,
+ flag, correct -> Log.d(TAG, "Server submission result: " + correct));
+
+ binding.verifyButton.setEnabled(false);
+ binding.pinInput.setEnabled(false);
+ } else {
+ // Failed authentication
+ binding.demoCard.setCardBackgroundColor(ContextCompat.getColor(requireContext(), R.color.error_bg));
+ binding.flagText.setVisibility(View.GONE);
+
+ Toast.makeText(getContext(), "Access Denied! Incorrect PIN", Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Authentication failed. Invalid PIN provided.");
+
+ binding.pinInput.setText("");
+ }
+ }
+
+ private void toggleFabExpansion(FloatingActionButton mainFab, FloatingActionButton fab1, FloatingActionButton fab2) {
+ fabExpanded = !fabExpanded;
+
+ if (fabExpanded) {
+ if (fab1 != null) fab1.setVisibility(View.VISIBLE);
+
+ if (mainFab != null) mainFab.setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
+ } else {
+ collapseFab(mainFab, fab1, fab2);
+ }
+ }
+
+ private void collapseFab(FloatingActionButton mainFab, FloatingActionButton fab1, FloatingActionButton fab2) {
+ fabExpanded = false;
+ if (fab1 != null) fab1.setVisibility(View.GONE);
+ if (fab2 != null) fab2.setVisibility(View.GONE);
+ if (mainFab != null) mainFab.setImageResource(android.R.drawable.ic_menu_help);
+ }
+
+
+ // Decode obfuscated flag - resist static analysis
+ private String d() {
+ try {
+ StringBuilder sb = new StringBuilder();
+ for (String p : F) {
+ sb.append(p);
+ }
+
+ byte[] decoded = Base64.getDecoder().decode(sb.toString());
+
+ byte[] result = new byte[decoded.length];
+ for (int i = 0; i < decoded.length; i++) {
+ result[i] = (byte) (decoded[i] ^ K);
+ }
+
+ return new String(result, StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ Log.e(TAG, "Flag decode error", e);
+ return "[DECODE_ERROR]";
+ }
+ }
+
+ private String hashPin(String pin) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(pin.getBytes(StandardCharsets.UTF_8));
+
+ // Convert byte array to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hashBytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "Error hashing PIN", e);
+ return null;
+ }
+ }
+
+ private void showDetailedInfo() {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_lesson_info, null);
+ TextView moduleBanner = dialogView.findViewById(R.id.module_path_banner);
+ if (moduleBanner != null) moduleBanner.setVisibility(View.VISIBLE);
+ if (moduleBanner != null) moduleBanner.setText("com.owasp.poor_authentication");
+
+ TextView introText = dialogView.findViewById(R.id.intro_text);
+ // TextView vulnerabilitiesText = dialogView.findViewById(R.id.vulnerabilities_text);
+ View hintsSection = dialogView.findViewById(R.id.hints_section);
+ TextView hintsText = dialogView.findViewById(R.id.hints_text);
+ View additionalSection = dialogView.findViewById(R.id.additional_section);
+
+ introText.setText(getString(R.string.poor_auth_intro));
+ // vulnerabilitiesText.setText(getString(R.string.poor_auth_vulnerabilities));
+
+ hintsSection.setVisibility(View.GONE);
+
+ additionalSection.setVisibility(View.GONE);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
+ builder.setTitle("Poor Authentication\nOWASP M3: Insecure Authentication/Authorization");
+ builder.setView(dialogView);
+ builder.setPositiveButton("Close", null);
+ builder.show();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+
+ // Hide mini FABs when leaving fragment
+ FloatingActionButton fab = requireActivity().findViewById(R.id.fab);
+ FloatingActionButton fabCommandRef = requireActivity().findViewById(R.id.fab_command_reference);
+ FloatingActionButton fabOwaspLink = requireActivity().findViewById(R.id.fab_owasp_link);
+ collapseFab(fab, fabCommandRef, fabOwaspLink);
+
+ binding = null;
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/privacycontrols/PrivacyControlsLessonFragment.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/privacycontrols/PrivacyControlsLessonFragment.java
new file mode 100644
index 000000000..64624ccd1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/ui/lessons/privacycontrols/PrivacyControlsLessonFragment.java
@@ -0,0 +1,258 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.preference.PreferenceManager;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Centralized flag validation system using SHA-256 hashes.
+ * Provides secure client-side validation and supports progress tracking.
+ */
+public class FlagValidator {
+
+ private static final String TAG = "FlagValidator";
+
+ // Module type constants
+ public static final String TYPE_LESSON = "lesson";
+ public static final String TYPE_CHALLENGE = "challenge";
+
+ // Module identifiers
+ public enum Module {
+ // Reverse Engineering
+ RE_LESSON("re_lesson", TYPE_LESSON),
+ RE_CHALLENGE_1("re_challenge_1", TYPE_CHALLENGE),
+
+ // Insecure Data Storage
+ IDS_LESSON("ids_lesson", TYPE_LESSON),
+ IDS_CHALLENGE_1("ids_challenge_1", TYPE_CHALLENGE),
+
+ // Poor Authentication
+ POOR_AUTH_LESSON("poor_auth_lesson", TYPE_LESSON),
+ POOR_AUTH_CHALLENGE("poor_auth_challenge", TYPE_CHALLENGE),
+
+ // Insecure Authorization
+ INSECURE_AUTH_LESSON("insecure_auth_lesson", TYPE_LESSON),
+
+ // Supply Chain
+ SUPPLY_CHAIN_LESSON("supply_chain_lesson", TYPE_LESSON),
+
+ // Insecure Communication
+ INSECURE_COMM_LESSON("insecure_comm_lesson", TYPE_LESSON),
+ INSECURE_COMM_CHALLENGE("insecure_comm_challenge", TYPE_CHALLENGE),
+
+ // Insufficient Cryptography
+ INSUFFICIENT_CRYPTO_LESSON("insufficient_crypto_lesson", TYPE_LESSON),
+ INSUFFICIENT_CRYPTO_CHALLENGE("insufficient_crypto_challenge", TYPE_CHALLENGE),
+
+ // Security Misconfiguration
+ SECURITY_MISCONFIG_LESSON("security_misconfig_lesson", TYPE_LESSON),
+ SECURITY_MISCONFIG_CHALLENGE_2("security_misconfig_challenge_2", TYPE_CHALLENGE),
+
+ // Input Validation
+ INPUT_VALIDATION_LESSON("input_validation_lesson", TYPE_LESSON),
+
+ // Privacy Controls
+ PRIVACY_LESSON("privacy_lesson", TYPE_LESSON),
+
+ // Client-Side Injection
+ CLIENT_SIDE_INJECTION_LESSON("client_side_injection_lesson", TYPE_LESSON),
+ CLIENT_SIDE_INJECTION_CHALLENGE_1("client_side_injection_challenge_1", TYPE_CHALLENGE),
+ CLIENT_SIDE_INJECTION_CHALLENGE_2("client_side_injection_challenge_2", TYPE_CHALLENGE);
+
+ private final String id;
+ private final String type;
+
+ Module(String id, String type) {
+ this.id = id;
+ this.type = type;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+ // RE_LESSON and RE_CHALLENGE_1 retain local hashes — the SHA-256 in the APK is the
+ // target of the reverse-engineering challenge itself. All other modules require a live
+ // server session; no offline fallback is provided.
+ private static final Map FLAG_HASHES = new HashMap() {{
+ put(Module.RE_LESSON, "a0c066b9cd89c084709330a943fb6b333d45c932ed613e428bc52078b5722e57");
+ put(Module.RE_CHALLENGE_1, "f04a272a2d82a0168f44559f9957dc9e028ecb13195c12fce17ac08d4af91deb");
+ }};
+
+ /**
+ * Validates a flag submission using SHA-256 hash comparison.
+ *
+ * @param module The module being validated
+ * @param submittedFlag The flag submitted by the user
+ * @return true if the flag is correct, false otherwise
+ */
+ public static boolean validateFlag(Module module, String submittedFlag) {
+ if (submittedFlag == null || submittedFlag.trim().isEmpty()) {
+ return false;
+ }
+
+ String expectedHash = FLAG_HASHES.get(module);
+ if (expectedHash == null) {
+ Log.e(TAG, "No hash found for module: " + module.getId());
+ return false;
+ }
+
+ String submittedHash = sha256(submittedFlag.trim());
+ boolean isValid = expectedHash.equalsIgnoreCase(submittedHash);
+
+ Log.d(TAG, isValid ? "[OK] Correct flag for " + module.getId()
+ : "[FAIL] Incorrect flag for " + module.getId());
+ return isValid;
+ }
+
+ /**
+ * Computes SHA-256 hash of the input string.
+ *
+ * @param input The string to hash
+ * @return Hexadecimal representation of the hash
+ */
+ private static String sha256(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+
+ // Convert bytes to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hashBytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "SHA-256 algorithm not available", e);
+ return "";
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Server-side validation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Callback interface for asynchronous flag validation results.
+ *
+ *
{@link #onResult(boolean)} is always invoked on the main (UI) thread.
+ */
+ public interface ValidationCallback {
+ void onResult(boolean correct);
+ }
+
+ /**
+ * Validates a flag against the configured Shepherd server when a session is active.
+ * Falls back to local SHA-256 comparison when no session is available,
+ * so the app remains usable without a running server instance.
+ *
+ *
When a server URL, username, and password are configured in the app preferences and a
+ * successful login has been completed, the app operates in online mode: flag values are
+ * fetched from the server as user-specific dynamic strings, and completion is validated
+ * server-side.
+ *
+ *
When no credentials are configured, or when the user explicitly logs out, the app operates in
+ * offline mode: static flag values are used and progress is tracked only on-device.
+ */
+public class AuthManager {
+
+ private static final String TAG = "AuthManager";
+
+ /**
+ * Preference key used to persist the authenticated state. Cleared on logout. The username,
+ * password and server URL are stored under their own preference keys so they are also visible
+ * in the Settings screen.
+ */
+ private static final String PREF_AUTH_LOGGED_IN = "auth_logged_in";
+ /** Preference key for the cached web JSESSIONID used by the scoreboard WebView. */
+ private static final String PREF_WEB_SESSION_COOKIE = "auth_web_session_cookie";
+ /** Preference key for the JSESSIONID captured at mobile login, used in API requests. */
+ private static final String PREF_MOBILE_SESSION_COOKIE = "auth_mobile_session_cookie";
+
+ public interface AuthCallback {
+ /** Always invoked on the main thread. */
+ void onResult(boolean success, String message);
+ }
+
+ // -------------------------------------------------------------------------
+ // State queries
+ // -------------------------------------------------------------------------
+
+ /** Returns {@code true} when a session cookie is held AND a successful login has been recorded. */
+ public static boolean isAuthenticated(Context ctx) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ return prefs.getBoolean(PREF_AUTH_LOGGED_IN, false)
+ && !getServerUrl(ctx).isEmpty()
+ && !getUsername(ctx).isEmpty()
+ && !getMobileSessionCookie(ctx).isEmpty();
+ }
+
+ public static String getServerUrl(Context ctx) {
+ return PreferenceManager.getDefaultSharedPreferences(ctx)
+ .getString("server_preference", "").trim();
+ }
+
+ /** Returns the username of the currently signed-in user, or empty string if not signed in. */
+ public static String getUsername(Context ctx) {
+ return PreferenceManager.getDefaultSharedPreferences(ctx)
+ .getString("username_preference", "").trim();
+ }
+
+ /**
+ * Returns the JSESSIONID captured during the last successful mobile login, formatted as
+ * a ready-to-use Cookie header value (e.g. {@code "JSESSIONID=abc123"}), or empty string
+ * if not yet obtained.
+ */
+ public static String getMobileSessionCookie(Context ctx) {
+ return PreferenceManager.getDefaultSharedPreferences(ctx)
+ .getString(PREF_MOBILE_SESSION_COOKIE, "");
+ }
+
+ /**
+ * Returns the cached web JSESSIONID cookie string (name=value) for use in the scoreboard
+ * WebView, or empty string if not yet obtained.
+ */
+ public static String getWebSessionCookie(Context ctx) {
+ return PreferenceManager.getDefaultSharedPreferences(ctx)
+ .getString(PREF_WEB_SESSION_COOKIE, "");
+ }
+
+ /** Persists a freshly-obtained web session cookie. Called by ScoreboardFragment. */
+ public static void saveWebSessionCookie(Context ctx, String cookie) {
+ PreferenceManager.getDefaultSharedPreferences(ctx).edit()
+ .putString(PREF_WEB_SESSION_COOKIE, cookie)
+ .apply();
+ }
+
+ /** Clears the cached web session cookie (e.g. on confirmed session expiry). */
+ public static void clearWebSessionCookie(Context ctx) {
+ PreferenceManager.getDefaultSharedPreferences(ctx).edit()
+ .remove(PREF_WEB_SESSION_COOKIE)
+ .apply();
+ }
+
+ // -------------------------------------------------------------------------
+ // Auth operations
+ // -------------------------------------------------------------------------
+
+ /**
+ * Attempts to authenticate against the Shepherd server. On success, stores the supplied
+ * credentials in the default shared preferences and sets the logged-in flag.
+ *
+ * @param ctx Context used to access preferences.
+ * @param serverUrl Base URL of the Shepherd server (e.g. {@code http://192.168.1.1:8080}).
+ * @param username Shepherd username.
+ * @param password Shepherd password.
+ * @param callback Receives the result on the main thread.
+ */
+ public static void login(
+ Context ctx,
+ String serverUrl,
+ String username,
+ String password,
+ AuthCallback callback) {
+
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+ new Thread(() -> {
+ HttpURLConnection conn = null;
+ try {
+ String endpoint = serverUrl.replaceAll("/+$", "") + "/mobileLogin";
+ String body = "login=" + URLEncoder.encode(username, "UTF-8")
+ + "&pwd=" + URLEncoder.encode(password, "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpoint).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) sb.append(line);
+ }
+
+ String responseBody = sb.toString();
+ if (status == HttpURLConnection.HTTP_OK && !responseBody.startsWith("ERROR")) {
+ // Capture the JSESSIONID for subsequent mobile API calls — avoids re-sending
+ // the raw password on every flag request.
+ String mobileCookie = "";
+ Map> respHeaders = conn.getHeaderFields();
+ if (respHeaders != null) {
+ List setCookies = respHeaders.get("Set-Cookie");
+ if (setCookies != null) {
+ for (String c : setCookies) {
+ if (c.regionMatches(true, 0, "JSESSIONID=", 0, 11)) {
+ mobileCookie = c.split(";")[0];
+ break;
+ }
+ }
+ }
+ }
+ // Persist server URL, username (for display), and session cookie
+ PreferenceManager.getDefaultSharedPreferences(ctx).edit()
+ .putString("server_preference", serverUrl.trim())
+ .putString("username_preference", username.trim())
+ .putBoolean(PREF_AUTH_LOGGED_IN, true)
+ .putString(PREF_MOBILE_SESSION_COOKIE, mobileCookie)
+ .apply();
+ Log.d(TAG, "Login successful for: " + username);
+ mainHandler.post(() -> callback.onResult(true, "Signed in as " + username));
+ } else {
+ Log.d(TAG, "Login rejected for: " + username + " (HTTP " + status + ")");
+ mainHandler.post(() -> callback.onResult(false, "Invalid username or password"));
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Login error: " + e.getMessage());
+ mainHandler.post(() -> callback.onResult(
+ false, "Could not reach server: " + e.getMessage()));
+ } finally {
+ if (conn != null) conn.disconnect();
+ }
+ }).start();
+ }
+
+ /**
+ * Attempts to register a new student account on the Shepherd server. On success, automatically
+ * logs in with the new credentials.
+ *
+ * @param ctx Context used to access preferences.
+ * @param serverUrl Base URL of the Shepherd server.
+ * @param username Desired username.
+ * @param password Desired password.
+ * @param email Email address (may be empty; validated server-side if provided).
+ * @param callback Receives the result on the main thread.
+ */
+ public static void register(
+ Context ctx,
+ String serverUrl,
+ String username,
+ String password,
+ String email,
+ AuthCallback callback) {
+
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+ new Thread(() -> {
+ HttpURLConnection conn = null;
+ try {
+ String endpoint = serverUrl.replaceAll("/+$", "") + "/mobileRegister";
+ String body = "userName=" + URLEncoder.encode(username, "UTF-8")
+ + "&passWord=" + URLEncoder.encode(password, "UTF-8")
+ + "&userAddress=" + URLEncoder.encode(email, "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpoint).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) sb.append(line);
+ }
+
+ if (status == HttpURLConnection.HTTP_OK) {
+ JSONObject json = new JSONObject(sb.toString());
+ if (json.optBoolean("success", false)) {
+ Log.d(TAG, "Registration successful for: " + username);
+ // Auto-login after successful registration
+ login(ctx, serverUrl, username, password, callback);
+ } else {
+ String msg = json.optString("message", "Registration failed");
+ mainHandler.post(() -> callback.onResult(false, msg));
+ }
+ } else {
+ mainHandler.post(() -> callback.onResult(
+ false, "Server error (HTTP " + status + ")"));
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Registration error: " + e.getMessage());
+ mainHandler.post(() -> callback.onResult(
+ false, "Could not reach server: " + e.getMessage()));
+ } finally {
+ if (conn != null) conn.disconnect();
+ }
+ }).start();
+ }
+
+ /**
+ * Clears the logged-in flag. Stored credentials are retained in preferences so the user does
+ * not need to retype them if they sign back in.
+ */
+ public static void logout(Context ctx) {
+ PreferenceManager.getDefaultSharedPreferences(ctx).edit()
+ .putBoolean(PREF_AUTH_LOGGED_IN, false)
+ .remove(PREF_WEB_SESSION_COOKIE)
+ .remove(PREF_MOBILE_SESSION_COOKIE)
+ .apply();
+ // Clear local lesson progress so the next user starts with a clean slate
+ ProgressTracker.clearAll(ctx);
+ // Also clear the WebView cookie store so the scoreboard can't be accessed after logout
+ android.webkit.CookieManager.getInstance().removeAllCookies(null);
+ Log.d(TAG, "User logged out");
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/CompletionBadge.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/CompletionBadge.java
new file mode 100644
index 000000000..0614f9604
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/CompletionBadge.java
@@ -0,0 +1,167 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.core.content.ContextCompat;
+
+import org.owasp.mobileshepherd.R;
+
+/**
+ * Manages completion badges and visual indicators for progress tracking.
+ */
+public class CompletionBadge {
+
+ public enum BadgeType {
+ FIRST_BLOOD("First Blood", "Complete your first challenge", android.R.drawable.star_on),
+ LESSON_MASTER("Lesson Master", "Complete all lessons", android.R.drawable.star_on),
+ CHALLENGE_CRUSHER("Challenge Crusher", "Complete all challenges", android.R.drawable.star_on),
+ SPEED_RUNNER("Speed Runner", "Complete a challenge in under 5 minutes", android.R.drawable.star_on),
+ PERFECTIONIST("Perfectionist", "100% completion", android.R.drawable.star_on),
+ SQL_NINJA("SQL Ninja", "Complete all injection challenges", android.R.drawable.star_on),
+ CRYPTO_BREAKER("Crypto Breaker", "Complete all cryptography challenges", android.R.drawable.star_on),
+ REVERSE_ENGINEER("Reverse Engineer", "Complete all RE challenges", android.R.drawable.star_on);
+
+ private final String title;
+ private final String description;
+ private final int iconResId;
+
+ BadgeType(String title, String description, int iconResId) {
+ this.title = title;
+ this.description = description;
+ this.iconResId = iconResId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getIconResId() {
+ return iconResId;
+ }
+ }
+
+ private final Context context;
+ private final ProgressTracker progressTracker;
+
+ public CompletionBadge(Context context) {
+ this.context = context;
+ this.progressTracker = new ProgressTracker(context);
+ }
+
+ /**
+ * Checks if user has earned a specific badge.
+ */
+ public boolean hasBadge(BadgeType badge) {
+ switch (badge) {
+ case FIRST_BLOOD:
+ return progressTracker.getTotalCompletedCount() >= 1;
+
+ case LESSON_MASTER:
+ return progressTracker.getCompletedLessonsCount() ==
+ progressTracker.getTotalLessonsCount();
+
+ case CHALLENGE_CRUSHER:
+ return progressTracker.getCompletedChallengesCount() ==
+ progressTracker.getTotalChallengesCount();
+
+ case PERFECTIONIST:
+ return progressTracker.getCompletionPercentage() == 100;
+
+ case SPEED_RUNNER:
+ // TODO: Implement time tracking for speed runs
+ return false;
+
+ case SQL_NINJA:
+ return isCategoryComplete("injection");
+
+ case CRYPTO_BREAKER:
+ return isCategoryComplete("crypto");
+
+ case REVERSE_ENGINEER:
+ return isCategoryComplete("re");
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Checks if all modules in a category are completed.
+ */
+ private boolean isCategoryComplete(String category) {
+ int total = 0;
+ int completed = 0;
+
+ for (FlagValidator.Module module : FlagValidator.Module.values()) {
+ if (module.getId().contains(category)) {
+ total++;
+ if (progressTracker.isCompleted(module)) {
+ completed++;
+ }
+ }
+ }
+
+ return total > 0 && completed == total;
+ }
+
+ /**
+ * Gets the completion icon for a module.
+ */
+ public static Drawable getCompletionIcon(Context context, boolean isCompleted) {
+ int resId = isCompleted ? android.R.drawable.checkbox_on_background : android.R.drawable.checkbox_off_background;
+ return ContextCompat.getDrawable(context, resId);
+ }
+
+ /**
+ * Gets a progress summary string.
+ */
+ public String getProgressSummary() {
+ int completed = progressTracker.getTotalCompletedCount();
+ int total = FlagValidator.Module.values().length;
+ int percentage = progressTracker.getCompletionPercentage();
+
+ StringBuilder summary = new StringBuilder();
+ summary.append("Progress: ").append(completed).append("/").append(total);
+ summary.append(" (").append(percentage).append("%)");
+
+ if (percentage == 100) {
+ summary.append(" ");
+ } else if (percentage >= 75) {
+ summary.append(" ");
+ } else if (percentage >= 50) {
+ summary.append(" [STRONG]");
+ } else if (percentage >= 25) {
+ summary.append(" [START]");
+ } else if (percentage > 0) {
+ summary.append(" ");
+ }
+
+ return summary.toString();
+ }
+
+ /**
+ * Gets motivational message based on progress.
+ */
+ public String getMotivationalMessage() {
+ int percentage = progressTracker.getCompletionPercentage();
+
+ if (percentage == 0) {
+ return "Start your security journey! ";
+ } else if (percentage < 25) {
+ return "Great start! Keep going! [START]";
+ } else if (percentage < 50) {
+ return "You're making progress! [STRONG]";
+ } else if (percentage < 75) {
+ return "Over halfway there! ";
+ } else if (percentage < 100) {
+ return "Almost done! Finish strong! ⭐";
+ } else {
+ return "Perfect score! You're a security expert! [TROPHY]";
+ }
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/CompletionVisualFeedback.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/CompletionVisualFeedback.java
new file mode 100644
index 000000000..98dab9060
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/CompletionVisualFeedback.java
@@ -0,0 +1,41 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+import androidx.core.content.ContextCompat;
+import org.owasp.mobileshepherd.R;
+
+/**
+ * Utility class for applying visual feedback to completed lessons and challenges
+ */
+public class CompletionVisualFeedback {
+
+ public static void applyCompletedBorder(View rootView, boolean isCompleted) {
+ if (isCompleted) {
+ // Create a green border drawable
+ GradientDrawable border = new GradientDrawable();
+ border.setShape(GradientDrawable.RECTANGLE);
+ border.setStroke(8, ContextCompat.getColor(rootView.getContext(), R.color.shepherd_green));
+ border.setCornerRadius(16f);
+
+ // Apply the border to the root view
+ rootView.setBackground(border);
+ rootView.setPadding(4, 4, 4, 4);
+ }
+ }
+
+ public static void applyCompletedBorderToCard(View cardView, boolean isCompleted) {
+ if (isCompleted && cardView != null) {
+ // For MaterialCardView, we can set stroke color
+ try {
+ com.google.android.material.card.MaterialCardView materialCard =
+ (com.google.android.material.card.MaterialCardView) cardView;
+ materialCard.setStrokeColor(ContextCompat.getColor(cardView.getContext(), R.color.shepherd_green));
+ materialCard.setStrokeWidth(8);
+ } catch (ClassCastException e) {
+ // If it's not a MaterialCardView, apply regular border
+ applyCompletedBorder(cardView, isCompleted);
+ }
+ }
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/FlagProvider.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/FlagProvider.java
new file mode 100644
index 000000000..520be4be9
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/FlagProvider.java
@@ -0,0 +1,167 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides the correct flag string for each mobile module in both offline and online modes.
+ *
+ *
Offline mode — when no server credentials are configured, an empty string is returned
+ * for all modules except the Reverse Engineering lesson, which retains a static offline flag for
+ * introductory use. Lessons are still explorable offline, but flag submission requires a server
+ * login so that completion is validated server-side.
+ *
+ *
Online mode — when the student is signed in, the server-generated user-specific flag
+ * is fetched from {@code /mobileFlagGet}. This flag is an HMAC of the base flag keyed with the
+ * server's ephemeral key and the student's username — unique per user, per server session, and
+ * unreachable via APK inspection.
+ *
+ *
In online mode the challenge DB is seeded with the dynamic flag before the student begins
+ * searching, so the value they discover via exploitation is always the server-validated flag.
+ */
+public class FlagProvider {
+
+ private static final String TAG = "FlagProvider";
+
+ /**
+ * Offline (static) flag values — only the Reverse Engineering lesson retains a static flag;
+ * its SHA-256 hash in the APK is the target of the reverse-engineering challenge itself.
+ * All other modules return an empty string when offline; a live server session is required.
+ */
+ private static final Map OFFLINE_FLAGS = new HashMap<>();
+
+ static {
+ OFFLINE_FLAGS.put(
+ FlagValidator.Module.RE_LESSON,
+ "Frozen_Clock_Melts_By_Noon");
+ }
+
+ public interface FlagCallback {
+ /** Always invoked on the main thread. */
+ void onFlag(String flag);
+ }
+
+ /**
+ * Returns the flag for the given module.
+ *
+ *
+ *
If the student is authenticated ({@link AuthManager#isAuthenticated}), a background
+ * HTTP request fetches the user-specific dynamic flag. On network failure the offline
+ * flag is returned as a fallback so the lesson remains usable.
+ *
Otherwise the offline static flag is returned synchronously (still via the main thread
+ * for API consistency).
+ *
+ *
+ * @param ctx Context used to read credentials.
+ * @param module The module whose flag is needed.
+ * @param callback Receives the flag string on the main thread.
+ */
+ public static void getFlag(Context ctx, FlagValidator.Module module, FlagCallback callback) {
+ String offlineFlag = OFFLINE_FLAGS.containsKey(module) ? OFFLINE_FLAGS.get(module) : "";
+
+ if (!AuthManager.isAuthenticated(ctx)) {
+ if (offlineFlag.isEmpty()) {
+ Log.d(TAG, "Offline mode — no static flag for " + module.getId()
+ + "; sign in to a Shepherd server to obtain a flag");
+ } else {
+ Log.d(TAG, "Offline mode — returning static flag for " + module.getId());
+ }
+ new Handler(Looper.getMainLooper()).post(() -> callback.onFlag(offlineFlag));
+ return;
+ }
+
+ String serverUrl = AuthManager.getServerUrl(ctx);
+ String sessionCookie = AuthManager.getMobileSessionCookie(ctx);
+ String startEndpoint = serverUrl.replaceAll("/+$", "") + "/mobileModuleStart";
+ String endpoint = serverUrl.replaceAll("/+$", "") + "/mobileFlagGet";
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ new Thread(() -> {
+ String resultFlag = offlineFlag;
+ HttpURLConnection conn = null;
+ try {
+ // Notify the server that this module has been opened. This must succeed
+ // before /mobileFlagGet will return a flag (prevents bulk API farming).
+ try {
+ String startBody = "moduleId=" + URLEncoder.encode(module.getId(), "UTF-8");
+ HttpURLConnection startConn =
+ (HttpURLConnection) new URL(startEndpoint).openConnection();
+ startConn.setRequestMethod("POST");
+ startConn.setDoOutput(true);
+ startConn.setConnectTimeout(5_000);
+ startConn.setReadTimeout(5_000);
+ startConn.setRequestProperty(
+ "Content-Type", "application/x-www-form-urlencoded");
+ if (!sessionCookie.isEmpty()) {
+ startConn.setRequestProperty("Cookie", sessionCookie);
+ }
+ try (OutputStream startOs = startConn.getOutputStream()) {
+ startOs.write(startBody.getBytes(StandardCharsets.UTF_8));
+ }
+ startConn.getResponseCode(); // consume response
+ startConn.disconnect();
+ } catch (Exception startEx) {
+ Log.w(TAG, "Module start notification failed for " + module.getId()
+ + ": " + startEx.getMessage());
+ }
+
+ String body = "moduleId=" + URLEncoder.encode(module.getId(), "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpoint).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ if (!sessionCookie.isEmpty()) {
+ conn.setRequestProperty("Cookie", sessionCookie);
+ }
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ if (status == HttpURLConnection.HTTP_OK) {
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) sb.append(line);
+ }
+ JSONObject json = new JSONObject(sb.toString());
+ String fetchedFlag = json.optString("flag", "");
+ if (!fetchedFlag.isEmpty()) {
+ resultFlag = fetchedFlag;
+ Log.d(TAG, "Dynamic flag fetched for " + module.getId());
+ }
+ } else {
+ Log.w(TAG, "Flag fetch HTTP " + status + " for " + module.getId()
+ + " — using offline flag");
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Flag fetch failed for " + module.getId()
+ + ": " + e.getMessage() + " — using offline flag");
+ } finally {
+ if (conn != null) conn.disconnect();
+ }
+
+ final String flag = resultFlag;
+ mainHandler.post(() -> callback.onFlag(flag));
+ }).start();
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/FlagValidator.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/FlagValidator.java
new file mode 100644
index 000000000..64624ccd1
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/FlagValidator.java
@@ -0,0 +1,258 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.preference.PreferenceManager;
+
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Centralized flag validation system using SHA-256 hashes.
+ * Provides secure client-side validation and supports progress tracking.
+ */
+public class FlagValidator {
+
+ private static final String TAG = "FlagValidator";
+
+ // Module type constants
+ public static final String TYPE_LESSON = "lesson";
+ public static final String TYPE_CHALLENGE = "challenge";
+
+ // Module identifiers
+ public enum Module {
+ // Reverse Engineering
+ RE_LESSON("re_lesson", TYPE_LESSON),
+ RE_CHALLENGE_1("re_challenge_1", TYPE_CHALLENGE),
+
+ // Insecure Data Storage
+ IDS_LESSON("ids_lesson", TYPE_LESSON),
+ IDS_CHALLENGE_1("ids_challenge_1", TYPE_CHALLENGE),
+
+ // Poor Authentication
+ POOR_AUTH_LESSON("poor_auth_lesson", TYPE_LESSON),
+ POOR_AUTH_CHALLENGE("poor_auth_challenge", TYPE_CHALLENGE),
+
+ // Insecure Authorization
+ INSECURE_AUTH_LESSON("insecure_auth_lesson", TYPE_LESSON),
+
+ // Supply Chain
+ SUPPLY_CHAIN_LESSON("supply_chain_lesson", TYPE_LESSON),
+
+ // Insecure Communication
+ INSECURE_COMM_LESSON("insecure_comm_lesson", TYPE_LESSON),
+ INSECURE_COMM_CHALLENGE("insecure_comm_challenge", TYPE_CHALLENGE),
+
+ // Insufficient Cryptography
+ INSUFFICIENT_CRYPTO_LESSON("insufficient_crypto_lesson", TYPE_LESSON),
+ INSUFFICIENT_CRYPTO_CHALLENGE("insufficient_crypto_challenge", TYPE_CHALLENGE),
+
+ // Security Misconfiguration
+ SECURITY_MISCONFIG_LESSON("security_misconfig_lesson", TYPE_LESSON),
+ SECURITY_MISCONFIG_CHALLENGE_2("security_misconfig_challenge_2", TYPE_CHALLENGE),
+
+ // Input Validation
+ INPUT_VALIDATION_LESSON("input_validation_lesson", TYPE_LESSON),
+
+ // Privacy Controls
+ PRIVACY_LESSON("privacy_lesson", TYPE_LESSON),
+
+ // Client-Side Injection
+ CLIENT_SIDE_INJECTION_LESSON("client_side_injection_lesson", TYPE_LESSON),
+ CLIENT_SIDE_INJECTION_CHALLENGE_1("client_side_injection_challenge_1", TYPE_CHALLENGE),
+ CLIENT_SIDE_INJECTION_CHALLENGE_2("client_side_injection_challenge_2", TYPE_CHALLENGE);
+
+ private final String id;
+ private final String type;
+
+ Module(String id, String type) {
+ this.id = id;
+ this.type = type;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+ // RE_LESSON and RE_CHALLENGE_1 retain local hashes — the SHA-256 in the APK is the
+ // target of the reverse-engineering challenge itself. All other modules require a live
+ // server session; no offline fallback is provided.
+ private static final Map FLAG_HASHES = new HashMap() {{
+ put(Module.RE_LESSON, "a0c066b9cd89c084709330a943fb6b333d45c932ed613e428bc52078b5722e57");
+ put(Module.RE_CHALLENGE_1, "f04a272a2d82a0168f44559f9957dc9e028ecb13195c12fce17ac08d4af91deb");
+ }};
+
+ /**
+ * Validates a flag submission using SHA-256 hash comparison.
+ *
+ * @param module The module being validated
+ * @param submittedFlag The flag submitted by the user
+ * @return true if the flag is correct, false otherwise
+ */
+ public static boolean validateFlag(Module module, String submittedFlag) {
+ if (submittedFlag == null || submittedFlag.trim().isEmpty()) {
+ return false;
+ }
+
+ String expectedHash = FLAG_HASHES.get(module);
+ if (expectedHash == null) {
+ Log.e(TAG, "No hash found for module: " + module.getId());
+ return false;
+ }
+
+ String submittedHash = sha256(submittedFlag.trim());
+ boolean isValid = expectedHash.equalsIgnoreCase(submittedHash);
+
+ Log.d(TAG, isValid ? "[OK] Correct flag for " + module.getId()
+ : "[FAIL] Incorrect flag for " + module.getId());
+ return isValid;
+ }
+
+ /**
+ * Computes SHA-256 hash of the input string.
+ *
+ * @param input The string to hash
+ * @return Hexadecimal representation of the hash
+ */
+ private static String sha256(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+
+ // Convert bytes to hex string
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hashBytes) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "SHA-256 algorithm not available", e);
+ return "";
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Server-side validation
+ // -------------------------------------------------------------------------
+
+ /**
+ * Callback interface for asynchronous flag validation results.
+ *
+ *
{@link #onResult(boolean)} is always invoked on the main (UI) thread.
+ */
+ public interface ValidationCallback {
+ void onResult(boolean correct);
+ }
+
+ /**
+ * Validates a flag against the configured Shepherd server when a session is active.
+ * Falls back to local SHA-256 comparison when no session is available,
+ * so the app remains usable without a running server instance.
+ *
+ *
This method is non-blocking. The result is delivered on the main thread via
+ * {@code callback}.
+ *
+ * @param context Application context used to read shared preferences.
+ * @param module The module being validated.
+ * @param flag The flag string submitted by the student.
+ * @param callback Receives {@code true} when the flag is correct, {@code false} otherwise.
+ */
+ public static void validateFlag(
+ Context context,
+ Module module,
+ String flag,
+ ValidationCallback callback) {
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String serverUrl = prefs.getString("server_preference", "").trim();
+ String sessionCookie = AuthManager.getMobileSessionCookie(context);
+
+ if (serverUrl.isEmpty() || sessionCookie.isEmpty()) {
+ // No active server session — only RE modules have offline hashes.
+ Log.d(TAG, "No server session — offline validation for " + module.getId());
+ boolean result = validateFlag(module, flag);
+ new Handler(Looper.getMainLooper()).post(() -> callback.onResult(result));
+ return;
+ }
+
+ final String endpointUrl = serverUrl.replaceAll("/+$", "") + "/mobileFlagSubmit";
+ final String moduleId = module.getId();
+ final String trimmedFlag = flag.trim();
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ new Thread(() -> {
+ boolean correct = false;
+ HttpURLConnection conn = null;
+ try {
+ String body =
+ "moduleId=" + URLEncoder.encode(moduleId, "UTF-8")
+ + "&flag=" + URLEncoder.encode(trimmedFlag, "UTF-8");
+
+ conn = (HttpURLConnection) new URL(endpointUrl).openConnection();
+ conn.setRequestMethod("POST");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(10_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ conn.setRequestProperty("Cookie", sessionCookie);
+
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int status = conn.getResponseCode();
+ if (status == HttpURLConnection.HTTP_OK) {
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ }
+ JSONObject json = new JSONObject(sb.toString());
+ correct = json.optBoolean("correct", false);
+ Log.d(TAG, "Server validation for " + moduleId + ": " + correct);
+ } else {
+ Log.w(TAG, "Server returned HTTP " + status + " for " + moduleId);
+ correct = validateFlag(module, trimmedFlag);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Server validation failed for " + moduleId + ": " + e.getMessage());
+ correct = validateFlag(module, trimmedFlag);
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+
+ final boolean result = correct;
+ mainHandler.post(() -> callback.onResult(result));
+ }).start();
+ }
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/ModuleInfoHelper.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/ModuleInfoHelper.java
new file mode 100644
index 000000000..1c7158bf9
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/ModuleInfoHelper.java
@@ -0,0 +1,210 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+public final class ModuleInfoHelper {
+
+ public static final class ModuleInfo {
+ public final String levelType;
+ public final String name;
+ public final String topTenRisk;
+ public final String location;
+
+ ModuleInfo(String levelType, String name, String topTenRisk, String location) {
+ this.levelType = levelType;
+ this.name = name;
+ this.topTenRisk = topTenRisk;
+ this.location = location;
+ }
+
+ public String toJson() {
+ return "{\n"
+ + " \"level_type\": \""
+ + levelType
+ + "\",\n"
+ + " \"name\": \""
+ + name
+ + "\",\n"
+ + " \"top_ten_risk\": \""
+ + topTenRisk
+ + "\",\n"
+ + " \"location\": \""
+ + location
+ + "\"\n"
+ + "}";
+ }
+ }
+
+ private static final Map REGISTRY =
+ new EnumMap<>(FlagValidator.Module.class);
+
+ static {
+ // Lessons
+ REGISTRY.put(
+ FlagValidator.Module.RE_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Reverse Engineering",
+ "M7",
+ "org.owasp.mobileshepherd.ui.lessons"));
+ REGISTRY.put(
+ FlagValidator.Module.IDS_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Insecure Data Storage",
+ "M9",
+ "org.owasp.mobileshepherd.ui.lessons.insecuredata"));
+ REGISTRY.put(
+ FlagValidator.Module.POOR_AUTH_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Poor Authentication",
+ "M3",
+ "org.owasp.mobileshepherd.ui.lessons.poorauth"));
+ REGISTRY.put(
+ FlagValidator.Module.INSECURE_AUTH_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Insecure Authorization",
+ "M3",
+ "org.owasp.mobileshepherd.ui.lessons.insecureauthorization"));
+ REGISTRY.put(
+ FlagValidator.Module.SUPPLY_CHAIN_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Supply Chain Security",
+ "M2",
+ "org.owasp.mobileshepherd.ui.lessons.supplychain"));
+ REGISTRY.put(
+ FlagValidator.Module.INSECURE_COMM_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Insecure Communication",
+ "M5",
+ "org.owasp.mobileshepherd.ui.lessons.insecurecomm"));
+ REGISTRY.put(
+ FlagValidator.Module.INSUFFICIENT_CRYPTO_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Insufficient Cryptography",
+ "M10",
+ "org.owasp.mobileshepherd.ui.lessons.crypto"));
+ REGISTRY.put(
+ FlagValidator.Module.SECURITY_MISCONFIG_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Security Misconfiguration",
+ "M8",
+ "org.owasp.mobileshepherd.ui.lessons.securitymisconfig"));
+ REGISTRY.put(
+ FlagValidator.Module.INPUT_VALIDATION_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Input Validation",
+ "M4",
+ "org.owasp.mobileshepherd.ui.lessons"));
+ REGISTRY.put(
+ FlagValidator.Module.PRIVACY_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Privacy Controls",
+ "M6",
+ "org.owasp.mobileshepherd.ui.lessons.privacycontrols"));
+ REGISTRY.put(
+ FlagValidator.Module.CLIENT_SIDE_INJECTION_LESSON,
+ new ModuleInfo(
+ "lesson",
+ "Client-Side Injection",
+ "M4",
+ "org.owasp.mobileshepherd.ui.lessons.clientsideinjection"));
+
+ // Challenges
+ REGISTRY.put(
+ FlagValidator.Module.RE_CHALLENGE_1,
+ new ModuleInfo(
+ "challenge",
+ "Reverse Engineering 1",
+ "M7",
+ "org.owasp.mobileshepherd.ui.challenges.reverseengineering"));
+ REGISTRY.put(
+ FlagValidator.Module.IDS_CHALLENGE_1,
+ new ModuleInfo(
+ "challenge",
+ "Insecure Data Storage 1",
+ "M9",
+ "org.owasp.mobileshepherd.ui.challenges.insecuredata1"));
+ REGISTRY.put(
+ FlagValidator.Module.POOR_AUTH_CHALLENGE,
+ new ModuleInfo(
+ "challenge",
+ "Poor Authentication",
+ "M3",
+ "org.owasp.mobileshepherd.ui.challenges.poorauth"));
+ REGISTRY.put(
+ FlagValidator.Module.INSECURE_COMM_CHALLENGE,
+ new ModuleInfo(
+ "challenge",
+ "Insecure Communication 1",
+ "M5",
+ "org.owasp.mobileshepherd.ui.challenges.insecurecomm"));
+ REGISTRY.put(
+ FlagValidator.Module.INSUFFICIENT_CRYPTO_CHALLENGE,
+ new ModuleInfo(
+ "challenge",
+ "Insufficient Cryptography 1",
+ "M10",
+ "org.owasp.mobileshepherd.ui.challenges.crypto"));
+ REGISTRY.put(
+ FlagValidator.Module.SECURITY_MISCONFIG_CHALLENGE_2,
+ new ModuleInfo(
+ "challenge",
+ "Security Misconfiguration",
+ "M8",
+ "org.owasp.mobileshepherd.ui.challenges.securitymisconfig"));
+ REGISTRY.put(
+ FlagValidator.Module.CLIENT_SIDE_INJECTION_CHALLENGE_1,
+ new ModuleInfo(
+ "challenge",
+ "Client-Side Injection 1",
+ "M4",
+ "org.owasp.mobileshepherd.ui.challenges.clientsideinjection"));
+ REGISTRY.put(
+ FlagValidator.Module.CLIENT_SIDE_INJECTION_CHALLENGE_2,
+ new ModuleInfo(
+ "challenge",
+ "Client-Side Injection 2",
+ "M4",
+ "org.owasp.mobileshepherd.ui.challenges.clientsideinjection"));
+ }
+
+ public static void showDialog(Context context, FlagValidator.Module module) {
+ ModuleInfo info = REGISTRY.get(module);
+ String json =
+ info != null ? info.toJson() : "{\n \"error\": \"Module info not found\"\n}";
+
+ TextView tv = new TextView(context);
+ tv.setText(json);
+ tv.setTypeface(Typeface.MONOSPACE);
+ tv.setPadding(48, 32, 48, 32);
+ tv.setTextSize(13f);
+
+ ScrollView scrollView = new ScrollView(context);
+ scrollView.addView(tv);
+
+ new AlertDialog.Builder(context)
+ .setTitle("Module Info")
+ .setView(scrollView)
+ .setPositiveButton("OK", null)
+ .show();
+ }
+
+ private ModuleInfoHelper() {}
+}
diff --git a/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/ProgressTracker.java b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/ProgressTracker.java
new file mode 100644
index 000000000..2ee67b2b8
--- /dev/null
+++ b/mobile/app/src/main/java/org/owasp/mobileshepherd/utils/ProgressTracker.java
@@ -0,0 +1,281 @@
+package org.owasp.mobileshepherd.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Tracks user progress through lessons and challenges.
+ * Stores completion status in SharedPreferences.
+ */
+public class ProgressTracker {
+
+ private static final String PREFS_NAME = "MobileShepherd_Progress";
+ private static final String KEY_COMPLETED_MODULES = "completed_modules";
+ private static final String KEY_FIRST_COMPLETION_TIME = "first_completion_";
+ private static final String KEY_LAST_COMPLETION_TIME = "last_completion_";
+ private static final String KEY_COMPLETION_COUNT = "completion_count_";
+
+ private final SharedPreferences prefs;
+ private static CompletionChangeListener globalListener;
+
+ public interface CompletionChangeListener {
+ void onCompletionChanged();
+ }
+
+ public static void setGlobalCompletionListener(CompletionChangeListener listener) {
+ globalListener = listener;
+ }
+
+ public static CompletionChangeListener getGlobalCompletionListener() {
+ return globalListener;
+ }
+
+ public ProgressTracker(Context context) {
+ this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Marks a module as completed.
+ *
+ * @param module The module that was completed
+ */
+ public void markCompleted(FlagValidator.Module module) {
+ Set completed = getCompletedModules();
+ boolean wasNew = completed.add(module.getId());
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putStringSet(KEY_COMPLETED_MODULES, completed);
+
+ long currentTime = System.currentTimeMillis();
+
+ // Track first completion time
+ if (wasNew) {
+ editor.putLong(KEY_FIRST_COMPLETION_TIME + module.getId(), currentTime);
+ }
+
+ // Always update last completion time
+ editor.putLong(KEY_LAST_COMPLETION_TIME + module.getId(), currentTime);
+
+ // Increment completion count
+ int count = getCompletionCount(module);
+ editor.putInt(KEY_COMPLETION_COUNT + module.getId(), count + 1);
+
+ editor.apply();
+
+ // Notify listener of change
+ if (globalListener != null) {
+ globalListener.onCompletionChanged();
+ }
+ }
+
+ /**
+ * Marks a module as not completed (undo completion).
+ *
+ * @param module The module to unmark
+ */
+ public void unmarkCompleted(FlagValidator.Module module) {
+ Set completed = getCompletedModules();
+ completed.remove(module.getId());
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putStringSet(KEY_COMPLETED_MODULES, completed);
+ editor.apply();
+
+ // Notify listener of change
+ if (globalListener != null) {
+ globalListener.onCompletionChanged();
+ }
+ }
+
+ /**
+ * Toggles completion status of a module.
+ *
+ * @param module The module to toggle
+ * @return true if now marked as completed, false if now marked as incomplete
+ */
+ public boolean toggleCompleted(FlagValidator.Module module) {
+ if (isCompleted(module)) {
+ unmarkCompleted(module);
+ return false;
+ } else {
+ markCompleted(module);
+ return true;
+ }
+ }
+
+ /**
+ * Checks if a module has been completed.
+ *
+ * @param module The module to check
+ * @return true if completed, false otherwise
+ */
+ public boolean isCompleted(FlagValidator.Module module) {
+ Set completed = getCompletedModules();
+ return completed.contains(module.getId());
+ }
+
+ /**
+ * Gets the set of all completed module IDs.
+ *
+ * @return Set of completed module IDs
+ */
+ public Set getCompletedModules() {
+ return new HashSet<>(prefs.getStringSet(KEY_COMPLETED_MODULES, new HashSet<>()));
+ }
+
+ /**
+ * Gets the timestamp of when a module was first completed.
+ *
+ * @param module The module to check
+ * @return Timestamp in milliseconds, or 0 if not completed
+ */
+ public long getFirstCompletionTime(FlagValidator.Module module) {
+ return prefs.getLong(KEY_FIRST_COMPLETION_TIME + module.getId(), 0);
+ }
+
+ /**
+ * Gets the timestamp of when a module was last completed.
+ *
+ * @param module The module to check
+ * @return Timestamp in milliseconds, or 0 if not completed
+ */
+ public long getLastCompletionTime(FlagValidator.Module module) {
+ return prefs.getLong(KEY_LAST_COMPLETION_TIME + module.getId(), 0);
+ }
+
+ /**
+ * Gets the number of times a module has been completed.
+ *
+ * @param module The module to check
+ * @return Completion count
+ */
+ public int getCompletionCount(FlagValidator.Module module) {
+ return prefs.getInt(KEY_COMPLETION_COUNT + module.getId(), 0);
+ }
+
+ /**
+ * Gets the total number of completed modules.
+ *
+ * @return Count of completed modules
+ */
+ public int getTotalCompletedCount() {
+ return getCompletedModules().size();
+ }
+
+ /**
+ * Gets the total number of completed challenges.
+ *
+ * @return Count of completed challenges
+ */
+ public int getCompletedChallengesCount() {
+ int count = 0;
+ for (String moduleId : getCompletedModules()) {
+ // Check if module is a challenge by looking it up
+ for (FlagValidator.Module module : FlagValidator.Module.values()) {
+ if (module.getId().equals(moduleId) &&
+ module.getType().equals(FlagValidator.TYPE_CHALLENGE)) {
+ count++;
+ break;
+ }
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Gets the total number of completed lessons.
+ *
+ * @return Count of completed lessons
+ */
+ public int getCompletedLessonsCount() {
+ int count = 0;
+ for (String moduleId : getCompletedModules()) {
+ // Check if module is a lesson by looking it up
+ for (FlagValidator.Module module : FlagValidator.Module.values()) {
+ if (module.getId().equals(moduleId) &&
+ module.getType().equals(FlagValidator.TYPE_LESSON)) {
+ count++;
+ break;
+ }
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Gets the total number of available challenges.
+ *
+ * @return Total challenge count
+ */
+ public int getTotalChallengesCount() {
+ int count = 0;
+ for (FlagValidator.Module module : FlagValidator.Module.values()) {
+ if (module.getType().equals(FlagValidator.TYPE_CHALLENGE)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Gets the total number of available lessons.
+ *
+ * @return Total lesson count
+ */
+ public int getTotalLessonsCount() {
+ int count = 0;
+ for (FlagValidator.Module module : FlagValidator.Module.values()) {
+ if (module.getType().equals(FlagValidator.TYPE_LESSON)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Calculates the completion percentage.
+ *
+ * @return Percentage (0-100)
+ */
+ public int getCompletionPercentage() {
+ int total = FlagValidator.Module.values().length;
+ int completed = getTotalCompletedCount();
+ if (total == 0) return 0;
+ return (int) ((completed * 100.0) / total);
+ }
+
+ /**
+ * Resets all progress (for testing or user request).
+ */
+ public void resetProgress() {
+ prefs.edit().clear().apply();
+ }
+
+ /**
+ * Clears all stored progress for any user. Call on logout so the next
+ * user starts with a clean state.
+ */
+ public static void clearAll(Context context) {
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().clear().apply();
+ }
+
+ /**
+ * Removes completion status for a specific module.
+ *
+ * @param module The module to reset
+ */
+ public void resetModule(FlagValidator.Module module) {
+ Set completed = getCompletedModules();
+ completed.remove(module.getId());
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putStringSet(KEY_COMPLETED_MODULES, completed);
+ editor.remove(KEY_FIRST_COMPLETION_TIME + module.getId());
+ editor.remove(KEY_LAST_COMPLETION_TIME + module.getId());
+ editor.remove(KEY_COMPLETION_COUNT + module.getId());
+ editor.apply();
+ }
+}
diff --git a/mobile/app/src/main/res/anim/fade_in.xml b/mobile/app/src/main/res/anim/fade_in.xml
new file mode 100644
index 000000000..49869de58
--- /dev/null
+++ b/mobile/app/src/main/res/anim/fade_in.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/mobile/app/src/main/res/anim/fade_out.xml b/mobile/app/src/main/res/anim/fade_out.xml
new file mode 100644
index 000000000..921fefcf7
--- /dev/null
+++ b/mobile/app/src/main/res/anim/fade_out.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/mobile/app/src/main/res/anim/scale_down.xml b/mobile/app/src/main/res/anim/scale_down.xml
new file mode 100644
index 000000000..05b4e7bf3
--- /dev/null
+++ b/mobile/app/src/main/res/anim/scale_down.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/mobile/app/src/main/res/anim/scale_up.xml b/mobile/app/src/main/res/anim/scale_up.xml
new file mode 100644
index 000000000..ab7e38836
--- /dev/null
+++ b/mobile/app/src/main/res/anim/scale_up.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/mobile/app/src/main/res/anim/slide_in_left.xml b/mobile/app/src/main/res/anim/slide_in_left.xml
new file mode 100644
index 000000000..6a893985c
--- /dev/null
+++ b/mobile/app/src/main/res/anim/slide_in_left.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/mobile/app/src/main/res/anim/slide_in_right.xml b/mobile/app/src/main/res/anim/slide_in_right.xml
new file mode 100644
index 000000000..cb6217ed8
--- /dev/null
+++ b/mobile/app/src/main/res/anim/slide_in_right.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/mobile/app/src/main/res/anim/slide_out_left.xml b/mobile/app/src/main/res/anim/slide_out_left.xml
new file mode 100644
index 000000000..25934a962
--- /dev/null
+++ b/mobile/app/src/main/res/anim/slide_out_left.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/mobile/app/src/main/res/anim/slide_out_right.xml b/mobile/app/src/main/res/anim/slide_out_right.xml
new file mode 100644
index 000000000..c22b0b507
--- /dev/null
+++ b/mobile/app/src/main/res/anim/slide_out_right.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable-v24/animation.xml b/mobile/app/src/main/res/drawable-v24/animation.xml
new file mode 100644
index 000000000..e3d8c3ec0
--- /dev/null
+++ b/mobile/app/src/main/res/drawable-v24/animation.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/drawable-v24/ic_launcher_background.xml b/mobile/app/src/main/res/drawable-v24/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/mobile/app/src/main/res/drawable-v24/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/mobile/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/drawable-v24/ic_menu.xml b/mobile/app/src/main/res/drawable-v24/ic_menu.xml
new file mode 100644
index 000000000..543cee9e8
--- /dev/null
+++ b/mobile/app/src/main/res/drawable-v24/ic_menu.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable-v24/icon.png b/mobile/app/src/main/res/drawable-v24/icon.png
new file mode 100644
index 000000000..5c9ce61a5
Binary files /dev/null and b/mobile/app/src/main/res/drawable-v24/icon.png differ
diff --git a/mobile/app/src/main/res/drawable-v24/pxart.png b/mobile/app/src/main/res/drawable-v24/pxart.png
new file mode 100644
index 000000000..659873a7b
Binary files /dev/null and b/mobile/app/src/main/res/drawable-v24/pxart.png differ
diff --git a/mobile/app/src/main/res/drawable/animation.xml b/mobile/app/src/main/res/drawable/animation.xml
new file mode 100644
index 000000000..e3d8c3ec0
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/animation.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/drawable/button_scale_animation.xml b/mobile/app/src/main/res/drawable/button_scale_animation.xml
new file mode 100644
index 000000000..41883dcc7
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/button_scale_animation.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/card_ripple.xml b/mobile/app/src/main/res/drawable/card_ripple.xml
new file mode 100644
index 000000000..054f97ceb
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/card_ripple.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/fire1.png b/mobile/app/src/main/res/drawable/fire1.png
new file mode 100644
index 000000000..c0f73f451
Binary files /dev/null and b/mobile/app/src/main/res/drawable/fire1.png differ
diff --git a/mobile/app/src/main/res/drawable/fire2.png b/mobile/app/src/main/res/drawable/fire2.png
new file mode 100644
index 000000000..378fe8d96
Binary files /dev/null and b/mobile/app/src/main/res/drawable/fire2.png differ
diff --git a/mobile/app/src/main/res/drawable/fire3.png b/mobile/app/src/main/res/drawable/fire3.png
new file mode 100644
index 000000000..ba10b5b4d
Binary files /dev/null and b/mobile/app/src/main/res/drawable/fire3.png differ
diff --git a/mobile/app/src/main/res/drawable/grass_tile_v2.jpeg b/mobile/app/src/main/res/drawable/grass_tile_v2.jpeg
new file mode 100644
index 000000000..a9ebe8880
Binary files /dev/null and b/mobile/app/src/main/res/drawable/grass_tile_v2.jpeg differ
diff --git a/mobile/app/src/main/res/drawable/grassbackground.xml b/mobile/app/src/main/res/drawable/grassbackground.xml
new file mode 100644
index 000000000..4294ea38b
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/grassbackground.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_expand_more.xml b/mobile/app/src/main/res/drawable/ic_expand_more.xml
new file mode 100644
index 000000000..3ebd02eb4
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_expand_more.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_launcher_background.xml b/mobile/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_alert.xml b/mobile/app/src/main/res/drawable/ic_menu_alert.xml
new file mode 100644
index 000000000..a5d7b6ace
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_alert.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_camera.xml b/mobile/app/src/main/res/drawable/ic_menu_camera.xml
new file mode 100644
index 000000000..ad626183e
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_camera.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_code.xml b/mobile/app/src/main/res/drawable/ic_menu_code.xml
new file mode 100644
index 000000000..c6c0f110c
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_code.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_database.xml b/mobile/app/src/main/res/drawable/ic_menu_database.xml
new file mode 100644
index 000000000..1151efa30
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_database.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_gallery.xml b/mobile/app/src/main/res/drawable/ic_menu_gallery.xml
new file mode 100644
index 000000000..75c7b1f74
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_gallery.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_home.xml b/mobile/app/src/main/res/drawable/ic_menu_home.xml
new file mode 100644
index 000000000..290e8808a
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_home.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_key.xml b/mobile/app/src/main/res/drawable/ic_menu_key.xml
new file mode 100644
index 000000000..c6b0e0119
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_key.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_link.xml b/mobile/app/src/main/res/drawable/ic_menu_link.xml
new file mode 100644
index 000000000..7f158e233
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_link.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_lock.xml b/mobile/app/src/main/res/drawable/ic_menu_lock.xml
new file mode 100644
index 000000000..baa139108
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_lock.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_network.xml b/mobile/app/src/main/res/drawable/ic_menu_network.xml
new file mode 100644
index 000000000..d31728a0b
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_network.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_settings.xml b/mobile/app/src/main/res/drawable/ic_menu_settings.xml
new file mode 100644
index 000000000..7996d6148
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_settings.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_shield.xml b/mobile/app/src/main/res/drawable/ic_menu_shield.xml
new file mode 100644
index 000000000..2dc9274ae
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_shield.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_slideshow.xml b/mobile/app/src/main/res/drawable/ic_menu_slideshow.xml
new file mode 100644
index 000000000..f00c2faba
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_slideshow.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_warning.xml b/mobile/app/src/main/res/drawable/ic_menu_warning.xml
new file mode 100644
index 000000000..78b19e650
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_warning.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_menu_web.xml b/mobile/app/src/main/res/drawable/ic_menu_web.xml
new file mode 100644
index 000000000..52eac72c3
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_menu_web.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_status_complete.xml b/mobile/app/src/main/res/drawable/ic_status_complete.xml
new file mode 100644
index 000000000..2c6157631
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_status_complete.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/ic_status_incomplete.xml b/mobile/app/src/main/res/drawable/ic_status_incomplete.xml
new file mode 100644
index 000000000..8f70e96d1
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/ic_status_incomplete.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/icon.png b/mobile/app/src/main/res/drawable/icon.png
new file mode 100644
index 000000000..5c9ce61a5
Binary files /dev/null and b/mobile/app/src/main/res/drawable/icon.png differ
diff --git a/mobile/app/src/main/res/drawable/nav_header_background.xml b/mobile/app/src/main/res/drawable/nav_header_background.xml
new file mode 100644
index 000000000..c22421ca4
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/nav_header_background.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/shape.xml b/mobile/app/src/main/res/drawable/shape.xml
new file mode 100644
index 000000000..c9c5a2792
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/shape.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/drawable/shepherd_and_sheep_v2.png b/mobile/app/src/main/res/drawable/shepherd_and_sheep_v2.png
new file mode 100644
index 000000000..d4e00a911
Binary files /dev/null and b/mobile/app/src/main/res/drawable/shepherd_and_sheep_v2.png differ
diff --git a/mobile/app/src/main/res/drawable/shepherd_background.xml b/mobile/app/src/main/res/drawable/shepherd_background.xml
new file mode 100644
index 000000000..85ce6f777
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/shepherd_background.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/drawable/side_nav_bar.xml b/mobile/app/src/main/res/drawable/side_nav_bar.xml
new file mode 100644
index 000000000..6d81870b0
--- /dev/null
+++ b/mobile/app/src/main/res/drawable/side_nav_bar.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/activity_landing.xml b/mobile/app/src/main/res/layout/activity_landing.xml
new file mode 100644
index 000000000..fac52e494
--- /dev/null
+++ b/mobile/app/src/main/res/layout/activity_landing.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/activity_login.xml b/mobile/app/src/main/res/layout/activity_login.xml
new file mode 100644
index 000000000..27a758435
--- /dev/null
+++ b/mobile/app/src/main/res/layout/activity_login.xml
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/activity_main.xml b/mobile/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..0e7b6ad45
--- /dev/null
+++ b/mobile/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/activity_misconfig_lesson_secret.xml b/mobile/app/src/main/res/layout/activity_misconfig_lesson_secret.xml
new file mode 100644
index 000000000..1f1515d86
--- /dev/null
+++ b/mobile/app/src/main/res/layout/activity_misconfig_lesson_secret.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/app_bar_main.xml b/mobile/app/src/main/res/layout/app_bar_main.xml
new file mode 100644
index 000000000..99ffc0081
--- /dev/null
+++ b/mobile/app/src/main/res/layout/app_bar_main.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/content_main.xml b/mobile/app/src/main/res/layout/content_main.xml
new file mode 100644
index 000000000..6e0ea390a
--- /dev/null
+++ b/mobile/app/src/main/res/layout/content_main.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/dialog_auth.xml b/mobile/app/src/main/res/layout/dialog_auth.xml
new file mode 100644
index 000000000..fa823a930
--- /dev/null
+++ b/mobile/app/src/main/res/layout/dialog_auth.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/dialog_lesson_info.xml b/mobile/app/src/main/res/layout/dialog_lesson_info.xml
new file mode 100644
index 000000000..42a826673
--- /dev/null
+++ b/mobile/app/src/main/res/layout/dialog_lesson_info.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_adb_reference.xml b/mobile/app/src/main/res/layout/fragment_adb_reference.xml
new file mode 100644
index 000000000..d54398877
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_adb_reference.xml
@@ -0,0 +1,1106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_client_side_injection_challenge1.xml b/mobile/app/src/main/res/layout/fragment_client_side_injection_challenge1.xml
new file mode 100644
index 000000000..19cac1fa7
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_client_side_injection_challenge1.xml
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_client_side_injection_challenge2.xml b/mobile/app/src/main/res/layout/fragment_client_side_injection_challenge2.xml
new file mode 100644
index 000000000..fc520db07
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_client_side_injection_challenge2.xml
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_client_side_injection_lesson.xml b/mobile/app/src/main/res/layout/fragment_client_side_injection_lesson.xml
new file mode 100644
index 000000000..afd3214b7
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_client_side_injection_lesson.xml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_home.xml b/mobile/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 000000000..412580eeb
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_input_validation_lesson.xml b/mobile/app/src/main/res/layout/fragment_input_validation_lesson.xml
new file mode 100644
index 000000000..0ce1362b0
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_input_validation_lesson.xml
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_insecure_authorization_lesson.xml b/mobile/app/src/main/res/layout/fragment_insecure_authorization_lesson.xml
new file mode 100644
index 000000000..ed23a543a
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_insecure_authorization_lesson.xml
@@ -0,0 +1,234 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_insecure_comm_challenge.xml b/mobile/app/src/main/res/layout/fragment_insecure_comm_challenge.xml
new file mode 100644
index 000000000..f57cecf64
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_insecure_comm_challenge.xml
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_insecure_comm_lesson.xml b/mobile/app/src/main/res/layout/fragment_insecure_comm_lesson.xml
new file mode 100644
index 000000000..c502089dd
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_insecure_comm_lesson.xml
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_insecure_data1.xml b/mobile/app/src/main/res/layout/fragment_insecure_data1.xml
new file mode 100644
index 000000000..1f6d0245e
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_insecure_data1.xml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_insecure_data_lesson.xml b/mobile/app/src/main/res/layout/fragment_insecure_data_lesson.xml
new file mode 100644
index 000000000..e5a464d10
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_insecure_data_lesson.xml
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_insufficient_crypto_challenge.xml b/mobile/app/src/main/res/layout/fragment_insufficient_crypto_challenge.xml
new file mode 100644
index 000000000..6dfb4780f
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_insufficient_crypto_challenge.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/fragment_insufficient_crypto_lesson.xml b/mobile/app/src/main/res/layout/fragment_insufficient_crypto_lesson.xml
new file mode 100644
index 000000000..0f93a3c07
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_insufficient_crypto_lesson.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_lesson.xml b/mobile/app/src/main/res/layout/fragment_lesson.xml
new file mode 100644
index 000000000..b44c4a115
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_lesson.xml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_poor_auth_challenge.xml b/mobile/app/src/main/res/layout/fragment_poor_auth_challenge.xml
new file mode 100644
index 000000000..fe9c2b03c
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_poor_auth_challenge.xml
@@ -0,0 +1,306 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_poor_auth_lesson.xml b/mobile/app/src/main/res/layout/fragment_poor_auth_lesson.xml
new file mode 100644
index 000000000..dc4a7f4f5
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_poor_auth_lesson.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_preferences.xml b/mobile/app/src/main/res/layout/fragment_preferences.xml
new file mode 100644
index 000000000..f0d3d062a
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_preferences.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/fragment_privacy_controls_lesson.xml b/mobile/app/src/main/res/layout/fragment_privacy_controls_lesson.xml
new file mode 100644
index 000000000..3a6a38f3a
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_privacy_controls_lesson.xml
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_progress.xml b/mobile/app/src/main/res/layout/fragment_progress.xml
new file mode 100644
index 000000000..df067bdf8
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_progress.xml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_reverse_engineering1.xml b/mobile/app/src/main/res/layout/fragment_reverse_engineering1.xml
new file mode 100644
index 000000000..223658d67
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_reverse_engineering1.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/fragment_scoreboard.xml b/mobile/app/src/main/res/layout/fragment_scoreboard.xml
new file mode 100644
index 000000000..219a7a349
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_scoreboard.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_security_misconfig_challenge2.xml b/mobile/app/src/main/res/layout/fragment_security_misconfig_challenge2.xml
new file mode 100644
index 000000000..459212462
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_security_misconfig_challenge2.xml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_security_misconfig_lesson.xml b/mobile/app/src/main/res/layout/fragment_security_misconfig_lesson.xml
new file mode 100644
index 000000000..3852c287c
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_security_misconfig_lesson.xml
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/fragment_supply_chain_lesson.xml b/mobile/app/src/main/res/layout/fragment_supply_chain_lesson.xml
new file mode 100644
index 000000000..d352249d1
--- /dev/null
+++ b/mobile/app/src/main/res/layout/fragment_supply_chain_lesson.xml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/item_module_progress.xml b/mobile/app/src/main/res/layout/item_module_progress.xml
new file mode 100644
index 000000000..50c0fe33c
--- /dev/null
+++ b/mobile/app/src/main/res/layout/item_module_progress.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/item_user_result.xml b/mobile/app/src/main/res/layout/item_user_result.xml
new file mode 100644
index 000000000..1bb90ff50
--- /dev/null
+++ b/mobile/app/src/main/res/layout/item_user_result.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/nav_drawer_layout.xml b/mobile/app/src/main/res/layout/nav_drawer_layout.xml
new file mode 100644
index 000000000..98dc03b9d
--- /dev/null
+++ b/mobile/app/src/main/res/layout/nav_drawer_layout.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/layout/nav_header_main.xml b/mobile/app/src/main/res/layout/nav_header_main.xml
new file mode 100644
index 000000000..40cc63bff
--- /dev/null
+++ b/mobile/app/src/main/res/layout/nav_header_main.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/layout/nav_item_layout.xml b/mobile/app/src/main/res/layout/nav_item_layout.xml
new file mode 100644
index 000000000..b23641bd4
--- /dev/null
+++ b/mobile/app/src/main/res/layout/nav_item_layout.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/app/src/main/res/menu/activity_main_drawer.xml b/mobile/app/src/main/res/menu/activity_main_drawer.xml
new file mode 100644
index 000000000..455e6b792
--- /dev/null
+++ b/mobile/app/src/main/res/menu/activity_main_drawer.xml
@@ -0,0 +1,108 @@
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/menu/main.xml b/mobile/app/src/main/res/menu/main.xml
new file mode 100644
index 000000000..02af0579b
--- /dev/null
+++ b/mobile/app/src/main/res/menu/main.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/mobile/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/mobile/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..f24ed12e6
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/mobile/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..81a8bad53
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/mobile/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..7084a23ca
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/mobile/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..cfba4732b
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..dd2c5aefa
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/app/src/main/res/navigation/mobile_navigation.xml b/mobile/app/src/main/res/navigation/mobile_navigation.xml
new file mode 100644
index 000000000..0c2d5b2a4
--- /dev/null
+++ b/mobile/app/src/main/res/navigation/mobile_navigation.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values-land/dimens.xml b/mobile/app/src/main/res/values-land/dimens.xml
new file mode 100644
index 000000000..22d7f0043
--- /dev/null
+++ b/mobile/app/src/main/res/values-land/dimens.xml
@@ -0,0 +1,3 @@
+
+ 48dp
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values-night/colors.xml b/mobile/app/src/main/res/values-night/colors.xml
new file mode 100644
index 000000000..dab97430b
--- /dev/null
+++ b/mobile/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,65 @@
+
+
+
+
+ #BA68C8
+ #AB47BC
+
+
+ #4CAF50
+ #81C784
+ #EF9A9A
+ #FFB74D
+ #90CAF9
+
+
+ #FF121212
+ #FF1E1E1E
+ #FFFFFFFF
+ #FFE0E0E0
+ #FFFFFFFF
+
+
+ #1E3A5F
+ #90CAF9
+ #42A5F5
+ #90CAF9
+
+ #4A1A1A
+ #EF9A9A
+
+ #1B5E20
+
+ #4A3A1A
+ #FFB74D
+ #FFB74D
+ #FFB74D
+
+ #1A3A1A
+ #81C784
+ #66BB6A
+
+
+ #0D1117
+ #7EE787
+ #81C784
+
+ #4A1A2A
+ #EC407A
+ #F06292
+
+ #FF2C2C2C
+ #FF2C2C2C
+ #FF616161
+
+ #FF2C2C2C
+ #4A1A1A
+ #EF9A9A
+ #1A3A1A
+ #81C784
+
+
+ #2196F3
+ #FF9800
+ #BA68C8
+
diff --git a/mobile/app/src/main/res/values-night/themes.xml b/mobile/app/src/main/res/values-night/themes.xml
new file mode 100644
index 000000000..05f7087b7
--- /dev/null
+++ b/mobile/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values-w1240dp/dimens.xml b/mobile/app/src/main/res/values-w1240dp/dimens.xml
new file mode 100644
index 000000000..d73f4a359
--- /dev/null
+++ b/mobile/app/src/main/res/values-w1240dp/dimens.xml
@@ -0,0 +1,3 @@
+
+ 200dp
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values-w600dp/dimens.xml b/mobile/app/src/main/res/values-w600dp/dimens.xml
new file mode 100644
index 000000000..22d7f0043
--- /dev/null
+++ b/mobile/app/src/main/res/values-w600dp/dimens.xml
@@ -0,0 +1,3 @@
+
+ 48dp
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values/colors.xml b/mobile/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..709549cc5
--- /dev/null
+++ b/mobile/app/src/main/res/values/colors.xml
@@ -0,0 +1,93 @@
+
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
+
+
+ #4A148C
+ #6A1B9A
+ #8E24AA
+
+
+ #4CAF50
+ #2E7D32
+ #81C784
+ #F44336
+ #C62828
+ #FF9800
+ #E65100
+ #2196F3
+ #1565C0
+
+
+ #311B92
+ #1A0033
+
+
+ #FFFFFFFF
+ #FFF5F5F5
+ #FF212121
+ #FF424242
+ #FFFFFFFF
+
+
+ #E3F2FD
+ #1565C0
+ #2196F3
+ #1565C0
+
+ #F3E5F5
+ #8E24AA
+
+ #FFEBEE
+ #C62828
+
+ #E8F5E9
+
+ #FFF3E0
+ #E65100
+ #FF9800
+ #E65100
+
+ #E8F5E9
+ #2E7D32
+
+
+ #1A1A2E
+ #A8D8A8
+ #4CAF50
+ #2E7D32
+
+ #FCE4EC
+ #E91E63
+ #C2185B
+
+ #FFFFFFFF
+ #FFFFFFFF
+ #9E9E9E
+
+ #F5F5F5
+ #FFEBEE
+ #D32F2F
+ #C8E6C9
+ #1B5E20
+
+
+ #2196F3
+ #FF9800
+ #8E24AA
+
+
+ #4CAF50
+ #E8F5E9
+ #E0E0E0
+ #FF5722
+ #2196F3
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values/dimens.xml b/mobile/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..8ce2ece8c
--- /dev/null
+++ b/mobile/app/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+
+ 16dp
+ 16dp
+ 8dp
+ 175dp
+ 16dp
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values/strings.xml b/mobile/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..621999c88
--- /dev/null
+++ b/mobile/app/src/main/res/values/strings.xml
@@ -0,0 +1,507 @@
+
+
+
+
+
+
+ MobileShepherd
+ OWASP Security Shepherd
+ Mobile Security Training
+ Navigation header
+ Open navigation drawer
+ Close navigation drawer
+
+
+ Settings
+ ADB Commands
+ Exit
+
+
+ Home
+ Scoreboard
+ Reverse Engineering Lesson
+ Reverse Engineering 1
+ Reverse Engineering 2
+ Reverse Engineering 3
+ Insecure Data Lesson
+ Insecure Data 1
+ Poor Authentication Lesson
+ Poor Authentication 1
+ Insecure Authorization Lesson
+ Supply Chain Security Lesson
+ Insecure Communication Lesson
+ Insecure Communication 1
+ Insufficient Cryptography Lesson
+ Insufficient Cryptography 1
+ Security Misconfiguration Lesson
+ Security Misconfiguration 1
+ Input Validation Lesson
+ Privacy Controls Lesson
+
+
+ Server Preferences
+ Preferences
+ Server Address
+ Enter Server address here
+ Enter Server Address
+
+
+
+ Offline mode
+ Signed in as: %1$s
+ Sign In
+ Sign Out
+ Sign In
+ Register
+ Server URL (e.g. http://10.0.2.2:8080)
+ Email (optional)
+ Registration only
+ No account? Register
+ Already have an account? Sign In
+ Submit an Issue
+ Report bugs or request features on GitHub
+ License
+ This App is part of the Security Shepherd Project.
+The Security Shepherd project is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version. The Security Shepherd
+project is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details. You should have received a copy of the GNU General
+Public License along with the Security Shepherd project. If not, see
+http://www.gnu.org/licenses.
+
+
+
+ System Default
+ Light
+ Dark
+
+
+ system
+ light
+ dark
+
+
+
+ Mobile Security Shepherd
+ WARNING: This is an intentionally vulnerable mobile application!
+ Enter App
+ This is a vulnerable App made to be used with the
+OWASP Security Shepherd Project. You will need to configure the server
+address in the settings to match the address of the Security Shepherd
+instance you have access to. This can be either a URL or an IP. For
+more information, please visit
+https://www.owasp.org/index.php/OWASP_Security_Shepherd
+ OWASP Security Shepherd Project
+ This application is designed for educational purposes as part of the OWASP Security Shepherd project.
+ Licensed under GNU GPL v3
+
+
+ Welcome to OWASP Mobile Shepherd
+ An Android App that hosts the Mobile Lesson and Challenge content from the OWASP Security Shepherd Project.
+
+
+
+ Your Progress
+ Track your learning journey through MobileShepherd
+ Overall Progress
+ 0%
+ 0 / 25 modules
+ Congratulations! You\'ve completed all modules!
+ Challenges
+ 0 / 14 completed
+ 0 of 13 challenges completed
+ Lessons
+ 0 / 11 completed
+ 0 of 11 lessons completed
+ Module Details
+ Module Name
+ Not Started
+ Reverse Engineering 1
+ Security Misconfiguration 2: Backup Extraction
+
+
+ Validate Key
+ Clear
+ Validate Flag
+ Submit Flag
+ Enter Flag
+ Flag
+ Results
+ Challenge Info
+ Help and Information
+ About
+ Hints
+ Command Reference
+ OWASP Mobile Top 10
+ Lesson Key Found:
+ KEY{...}
+ What Just Happened?
+ Vulnerability explanation...
+ Actions
+ Cancel
+ Login
+
+ Get Started
+ 1) Ensure this App can reach the Security Shepherd Platform.
+ 2) Register an account or sign in with your existing credentials.
+ 3) Sign in below.
+ 3) Register below.
+ Continue without signing in (offline mode)
+ Please fill in Server URL, Username and Password.
+ Connecting\u2026
+
+ Logout
+ Username
+ Password
+
+
+ ADB Command Reference
+ Quick reference for Android Debug Bridge commands used in challenges. Tap any command to copy it to clipboard.
+ Logcat Commands
+ View all logs
+ adb logcat
+ Filter by tag
+ adb logcat -s TAG_NAME
+ Search logs (PowerShell)
+ adb logcat | grep "search_term"
+ Clear logs
+ adb logcat -c
+ Basic ADB Commands
+ List connected devices
+ adb devices
+ Open device shell
+ adb shell
+ Install APK
+ adb install app.apk
+ Uninstall app
+ adb uninstall com.package.name
+ File Operations
+ Pull database file
+ adb pull /data/data/org.owasp.mobileshepherd/databases/Users.db
+ Pull SharedPreferences
+ adb pull /data/data/org.owasp.mobileshepherd/shared_prefs/
+ List app files
+ adb shell ls /data/data/org.owasp.mobileshepherd/
+ List databases
+ adb shell ls /data/data/org.owasp.mobileshepherd/databases/
+ Database Commands
+ Open SQLite database
+ adb shell sqlite3 /data/data/org.owasp.mobileshepherd/databases/Users.db
+ Query database
+ sqlite> SELECT * FROM users;
+ View file contents
+ adb shell cat /data/data/org.owasp.mobileshepherd/shared_prefs/UserCredentials.xml
+ Package Management
+ Find package name
+ adb shell pm list packages | grep reverser
+ Get APK path
+ adb shell pm path org.owasp.mobileshepherd
+ Pull APK file
+ adb pull /data/app/org.owasp.mobileshepherd-*/base.apk
+ Clear app data
+ adb shell pm clear org.owasp.mobileshepherd
+
+
+ Insufficient Cryptography Challenge
+ Read vault from device (run-as)
+ adb shell run-as org.owasp.mobileshepherd cat shared_prefs/recovery_vault.xml
+ Pull vault XML to host
+ adb pull /data/data/org.owasp.mobileshepherd/shared_prefs/recovery_vault.xml
+ Decompile APK (find XOR key)
+ jadx-gui app-debug.apk
+ XOR-decode vault data (Python)
+ python3 -c "import base64; d=base64.b64decode(\'PASTE_BASE64_HERE\'); print(\'\'.join(chr(b^0x42) for b in d))"
+
+
+
+
+
+
+
+ The Reverse Engineering module introduces techniques for extracting hidden information from Android applications.
+ - APKTool - Decompiles APK files\n- Jadx - Java decompiler\n- dex2jar - Converts DEX to JAR\n- JD-GUI - Java class viewer\n- Android Studio - APK Analyzer
+ Step-by-Step Guide:\n\n1. EXTRACT THE APK\n - Connect device via ADB\n - Run: adb shell pm list packages | grep reverser\n - Run: adb shell pm path org.owasp.mobileshepherd\n - Run: adb pull /data/app/[path]/base.apk\n\n2. DECOMPILE WITH JADX\n - Download JADX from GitHub\n - Run: jadx base.apk\n - Or use JADX-GUI for visual interface\n\n3. NAVIGATE TO TARGET FILE\n - Open: sources/com/owasp/reverser/ui/lessons/LessonFragment.java\n - Look in the class body for string variables\n - Search for keywords: flag, key, secret, password\n\n4. ALTERNATIVE TOOLS\n - APKTool: apktool d base.apk (produces smali)\n - dex2jar + JD-GUI: For JAR decompilation\n - Ghidra: Advanced static analysis\n - Frida: Dynamic runtime inspection\n\n5. WHAT TO LOOK FOR\n - Hardcoded strings in class fields\n - Static final String variables\n - Suspicious base64 or hex strings\n - Comments or debug messages\n\nTarget: LessonFragment.java in package org.owasp.mobileshepherd.ui.lessons
+ Where to Find the Key
+ The key is hardcoded in the Java source code of this lesson fragment:\n\nFile: LessonFragment.java\nPackage: org.owasp.mobileshepherd.ui.lessons\nLocation: Inside the class as a string variable
+ Verify Key
+ Enter the key you found
+
+
+ The key is hardcoded in the Java source code of this app. Decompile the APK to find it.
+ Hint: The key is stored as a simple string constant in one of the Java classes. Try searching for "flag" or "key" in the decompiled code.
+ Enter key here
+ Key validated successfully.
+ The key is incorrect. Please try again.
+
+
+
+
+
+
+
+
+
+ Learn about insecure data storage vulnerabilities in mobile applications. This is one of the most common security issues in mobile apps where sensitive information is stored without proper protection.
+ - SQLite Databases (unencrypted)\n- SharedPreferences (plain text)\n- Internal Storage files\n- External Storage (SD card)\n- Application logs\n- Temporary files\n- Cloud storage backups
+ 1. Physical Access - Anyone with the device can extract data\n2. Backup Exposure - Data in cloud backups\n3. Malware - Other apps can access unprotected data\n4. Forensics - Data persists even after app deletion\n5. Rooted Devices - Full filesystem access
+
+
+ Insecure Data Storage 1
+ This app stores a password in a local SQLite database. The password has been hashed, but the algorithm is weak. Extract the database and crack the hash to find the key.
+ \nPasswords are hidden here but stored in plaintext in:\n/data/data/org.owasp.mobileshepherd/databases/passwordDB
+ This challenge shows password hash cracking. The app stores a password hash in a database - your goal is to crack the hash and find the original password.
+ Data is stored in plaintext in a local SQLite database. Get access to this data to find the key.
+ Database Users
+ Members stored in unencrypted SQLite database:
+ \nPasswords are hidden here but stored in plaintext in:\n/data/data/org.owasp.mobileshepherd/databases/Members.db
+
+
+
+
+
+
+
+
+ Poor authentication is one of the most critical mobile vulnerabilities. Authentication is the cornerstone of app security - if attackers bypass it, all other security measures become irrelevant.
+ - Weak security questions (easily guessable)\n- Passwords logged in plaintext\n- Credentials in world-readable files\n- Weak temporary passwords\n- Verbose error messages\n- Debug logs in production
+
+
+ An account in this app is locked. The password reset workflow leaks the answers to its security questions through logs and local files. Recover the answers, reset the password, and authenticate to retrieve the key.
+ Poor Authentication
+ Forgot Password?
+ Password Reset
+ Answer these security questions to reset your password:
+ What is your favorite food?
+ What is your mother\'s maiden name?
+ Reset Password
+ Login Successful!
+ This app protects a secret behind a PIN. The authentication mechanism has a fundamental weakness that makes it possible to bypass. Find the PIN to retrieve the key.
+ Validate Pin
+ This exercise shows hardcoded credentials and insecure logging. Try to find the PIN by exploiting these vulnerabilities!
+ Enter PIN
+ Verify PIN
+ Device Information
+ Serial: Loading...
+ Build: Loading...
+ Manufacturer: Loading...
+ Brand: Loading...
+ SDK: Loading...
+
+
+
+
+
+
+
+ Insecure authorization occurs when apps fail to properly enforce access controls after authentication. Even if users authenticate correctly, improper authorization checks allow privilege escalation, access to other users\' data, or unauthorized actions. Client-side authorization flaws can allow role and permission checks to be manipulated.
+
+
+ This app has two privilege levels. A standard user account is available to log in with, but the key is only accessible to admins. Find a way to escalate your privileges.
+ Login to App
+ User Dashboard
+ Welcome, User!
+ Current Role: user
+ Access Admin Panel
+ Admin Panel
+ Access Denied\n\nYou do not have admin privileges.\n\nHint: Authorization is checked client-side using SharedPreferences.
+
+
+
+
+
+
+
+ Inadequate supply chain security occurs when mobile apps use vulnerable third-party libraries, SDKs, or dependencies. Attackers exploit these weak links to compromise entire applications, affecting millions of users.
+ Common Issues:\n\n- Using dependencies with known vulnerabilities\n- Not verifying SDK authenticity\n- Hardcoded API keys in libraries\n- Insecure data collection by third parties\n- Excessive permissions requested by SDKs\n- Outdated dependencies not updated for years
+
+ OWASP Mobile Top 10: M6 - Insufficient Supply Chain Security
+ This app bundles a third-party library that has a known vulnerability. Identify the vulnerable dependency and exploit it to retrieve the key.
+ Vulnerable SDK
+ SDK Investigation
+ The app uses a vulnerable third-party analytics SDK. Analyze the SDK initialization, check logs, and inspect the filesystem to find security issues.
+ Analyze SDK
+ Analysis Results
+ Once you\'ve exploited the supply chain vulnerability, enter the flag below:
+ Third-Party Dependencies
+ androidx.appcompat:appcompat:1.6.1
+ com.google.android.material:material:1.11.0
+ androidx.navigation:navigation-fragment:2.7.7
+ androidx.exifinterface:exifinterface:1.3.7
+ androidx.constraintlayout:constraintlayout:2.1.4
+
+
+
+
+
+
+
+ Insecure communication occurs when mobile apps transmit sensitive data without proper encryption. Any attacker on the same network can intercept, read, and modify unencrypted traffic, leading to credential theft, session hijacking, and data breaches.
+ Common Issues:\n\n- Sending credentials over HTTP\n- Accepting invalid/self-signed certificates\n- Using weak cipher suites or deprecated TLS versions\n- Disabling certificate pinning for convenience\n- Sensitive data in URLs (visible in logs/history)\n- Mixed HTTP/HTTPS sessions exposing session tokens
+
+
+ This app sends traffic to multiple endpoints, some over HTTP. Set up a proxy and capture the unencrypted request to find the key.
+ Network Traffic Analysis
+ Network Monitor
+ This app makes multiple network requests. Start monitoring to capture all traffic and identify insecure communications.
+ Start Monitoring
+ Ready to monitor network traffic...
+ Analyze the captured traffic in logcat. Find the request sent over HTTP (not HTTPS) and extract the session token.
+ This app transmits sensitive data over the network without adequate protection. Intercept the app\'s traffic to find the key.
+ Analytics Event
+ Set up a proxy (e.g., Burp Suite on 127.0.0.1:8080) and configure the emulator to use it. When you click the button below, the app will send analytics data to a third-party service over HTTP with the API key in the headers.
+ Send Analytics Event
+ Ready to send request...
+
+
+
+
+
+
+
+ Insufficient cryptography occurs when mobile apps use deprecated encryption algorithms, inadequate key lengths, poor key management, or flawed cryptographic implementations. These vulnerabilities allow attackers to decrypt sensitive data, bypass encryption, or manipulate cryptographic processes.
+ Common Crypto Vulnerabilities:\n\n- Deprecated algorithms: DES, RC4, MD5, SHA-1\n- Insufficient key length: Keys < 128 bits\n- ECB mode: Pattern analysis attacks\n- Hardcoded keys: Keys in source code\n- Static IVs: Initialization vector reuse\n- Poor key derivation: Simple hashing for keys\n- Insecure random: Predictable Random() instead of SecureRandom\n- No salt: Password hashing without random salt
+
+
+ This app stores your recovery keys in local storage using a custom encoding scheme. Save your keys, then investigate how the data is actually stored on the device. Can you recover the hidden session token from the vault?
+ Insufficient Cryptography Challenge
+ Once you\'ve identified all cryptographic vulnerabilities and extracted the hidden flag, submit it below.
+ Enter flag (format: KEY{...})
+ Sensitive data in this app is encrypted, but the encryption algorithm and key length are weak. Break the encryption to recover the hidden secrets.
+ Encrypt / Decrypt Data
+ Enter a key (0-8 characters), text, and optional hint. Click on secrets below to decrypt them.
+ Enter key (0-8 characters)
+ Enter text
+ Enter hint (optional)
+ Encrypt
+ Decrypt
+ Output will appear here
+ Encrypted Secrets
+ Decrypt these using the correct key:
+
+
+
+
+
+
+
+ Security misconfiguration occurs when security settings are not properly defined, implemented, maintained, or enforced. This is one of the most common vulnerabilities in mobile applications, often resulting from default configurations, incomplete setups, or overly permissive settings that expose the app to various attack vectors.
+ Common Security Misconfigurations:\n\n- Debug Mode Enabled: android:debuggable=true allows JDWP debugging in production\n- Backup Allowed: android:allowBackup=true enables ADB backup extraction of app data\n- Exported Components: Activities/Services/Receivers without proper permission checks\n- Cleartext Traffic: Allowing unencrypted HTTP connections\n- Excessive Permissions: Requesting unnecessary dangerous permissions\n- Verbose Error Messages: Leaking sensitive information in production logs\n- Default Credentials: Unchanged API keys, passwords in configuration files\n- Insecure File Permissions: World-readable/writable files\n- Missing Certificate Pinning: Vulnerable to MITM attacks\n- WebView Misconfigurations: JavaScript enabled, file access allowed
+ Configuration Check
+ Check various security configuration settings to identify which ones are properly configured and which are security risks.
+ Check Configuration
+ Debug Mode: Checking...
+ Backup Allowed: Checking...
+ Exported Components: Checking...
+ Network Security: Checking...
+ Permissions: Checking...
+ Exported Component Exploited!
+ Exploit Exported Component
+ This app contains an EXPORTED COMPONENT that can be invoked by any app or ADB command. Scan for components, identify the vulnerable one, and exploit it to retrieve the lesson key!
+ Scan App Components
+ Discovered Activities:
+ Safe (exported=false) / Vulnerable (exported=true)
+ ADB Exploitation Command:
+ adb shell am start -n org.owasp.mobileshepherd/[ACTIVITY_PATH]
+ Scan components above, find the exported one, and replace [ACTIVITY_PATH] with the full class path (e.g., .ui.lessons.securitymisconfig.ClassName)
+ This app has been misconfigured in ways that expose it to attack. Examine the app\'s configuration to find the key.
+
+
+ This app has ADB backup enabled. Extract a backup of the app data and examine the contents to find the key stored in plaintext.
+ Stored User Preferences:
+ Username: admin
+ Theme: Dark Mode
+ Language: English
+ Secret Flag: [Encrypted]
+ Hint: This app allows backups. Use ADB to extract the app data and examine the backup file for stored preferences.
+
+
+
+
+
+
+
+ Input/Output validation vulnerabilities occur when applications fail to properly validate, sanitize, or encode data at trust boundaries. This includes both data coming into the app (user input, API responses, intents) and data going out (WebView content, logs, broadcasts). Insufficient validation can lead to injection attacks, data corruption, security bypasses, and unintended application behavior.
+ Common Input/Output Validation Issues:\n\n- SQL Injection: Unsanitized input in database queries allows data extraction\n- Path Traversal: Unvalidated file paths enable access to unauthorized files\n- WebView XSS: Unescaped user data rendered in WebViews enables script injection\n- Intent Injection: Malicious data in Android Intents bypasses security checks\n- Command Injection: User input passed to system commands enables arbitrary execution\n- XML/JSON Injection: Malformed data breaks parsers and logic\n- Log Injection: Unvalidated data in logs creates fake entries or leaks data\n- URL Validation Bypass: Weak URL checks allow access to unauthorized resources\n- Format String Attacks: User input in format strings causes crashes or leaks\n- Deep Link Exploits: Improperly validated deep link parameters bypass authentication
+
+ Challenge
+
+
+ Deep Link Content Loader
+ This app loads external content via deep links, but only permits a set of trusted domains. Find a way to bypass this restriction.
+ Quick Links
+ Example.com
+ Trusted Site
+ OWASP.org
+ Custom Deep Link
+ Format: myapp://open?url=<target_url>
+ Enter target URL
+ Open Deep Link
+ Loaded Content
+ No content loaded
+ Use the buttons above or enter a custom deep link to load content.
+
+
+
+
+
+
+
+ Inadequate Privacy Controls
+ A photo is accessible within this app. It was shared without being properly sanitised first. Examine it carefully to find the key.
+ Image metadata can leak sensitive information about users, including location, device information, and timestamps. The content aligns with OWASP Mobile Top 10 M6: Inadequate Privacy Controls.
+ Load the sample image and analyze its metadata to find the hidden key. Pay attention to EXIF data that applications often fail to strip before sharing or uploading images.
+ Privacy Control Failures:\n\n- Image Metadata Leakage: Photos contain GPS coordinates, timestamps, device info in EXIF data\n- Location Tracking: Apps collect precise location without clear user benefit\n- Clipboard Access: Reading clipboard without user knowledge\n- Contact/Calendar Access: Uploading full contact lists unnecessarily\n- Analytics Overreach: Tracking user behavior beyond stated purpose\n- PII in Logs: Logging personally identifiable information\n- Identifier Persistence: Using permanent device IDs for tracking\n- No Privacy Controls: Missing opt-out mechanisms\n- Data Minimization Failure: Collecting more data than needed\n- Third-party SDK Leaks: SDKs transmitting user data without disclosure
+ Finding the Key:\n\n1. Tap "Load Sample Image" to load the preloaded photo\n2. Tap "Analyze Metadata" to view all EXIF data\n3. Look for the Image Description field in the metadata\n4. The key is hidden in one of the EXIF tags
+ Photo with metadata
+ No image loaded. Tap \"Load Sample Image\" to begin.
+ Load Sample Image
+ Take New Photo
+ Extract EXIF metadata from images to discover location coordinates. Use ADB to pull the photo and examine its metadata.
+ Photo Viewer
+
+
+
+
+
+
+
+ This app queries a local database using unsanitised user input. Manipulate the query to expose data that isn\'t meant to be visible.
+ Client-Side Injection occurs when mobile applications fail to properly validate and sanitize user input before using it in SQL queries, JavaScript contexts, or other interpreters on the device.\n\nTypes of Client-Side Injection:\n\n- SQL Injection in SQLite databases\n- JavaScript injection in WebViews\n- XML/JSON injection in parsers\n- Command injection in system calls\n- NoSQL injection in local databases\n- File path injection\n\nSQL Injection is particularly dangerous because:\n- SQLite databases store sensitive app data\n- Attackers can read any table\n- Authentication can be bypassed\n- Data can be modified or deleted\n- Business logic can be circumvented
+ SQL Injection Vulnerabilities:\n\n- String Concatenation: Building queries with user input\n - "SELECT * FROM users WHERE name = \'" + input + "\'"\n - Allows injection: \' OR 1=1--\n\n- Improper Escaping: Not handling special characters\n - Single quotes (\')\n - Comment markers (-- or /**/)\n - SQL keywords (OR, UNION, SELECT)\n\n- Lack of Validation: Accepting any user input\n - No input length limits\n - No character whitelisting\n - No pattern matching\n\nCommon Attack Patterns:\n- Authentication Bypass: \' OR \'1\'=\'1\n- UNION Attacks: \' UNION SELECT * FROM secrets--\n- Comment Injection: admin\'--\n- Always True Conditions: 1\' OR \'1\'=\'1
+
+
+ Client-Side Injection 1
+ This app authenticates users against a local SQLite database using a query built with string concatenation. Bypass the login to retrieve the admin password.
+ A vulnerable login workflow builds SQL queries through string concatenation. User accounts are stored in a SQLite database with usernames, passwords, roles, and balance data.\n\nObjective:\n1. Identify the SQL injection vulnerability\n2. Bypass the authentication mechanism\n3. Retrieve the administrator account password\n4. Submit the password as the key\n\nThe weakness exists because user input is concatenated directly into SQL queries without proper validation or parameterization.
+ Vulnerable Code Pattern:\n\nString query = "SELECT * FROM accounts WHERE username = \'" + username + "\' AND password = \'" + password + "\'";\n\nThis allows attackers to:\n- Break out of the query context\n- Inject their own SQL logic\n- Bypass authentication checks\n- Retrieve data from any user\n\nExample Attack:\nUsername: admin\'--\nPassword: (anything)\n\nResulting Query:\nSELECT * FROM accounts WHERE username = \'admin\'--\' AND password = \'....\'\n\nThe -- comments out the password check!
+ Account Login
+ Login to access your account. Can you bypass the authentication and retrieve the admin password?
+ Login results will appear here...
+ Once you\'ve retrieved the admin password, submit it as the flag:
+
+
+ Client-Side Injection 2
+ This app searches a product database using unsanitised input. Use a UNION-based injection to query the hidden secrets table and extract the admin token.
+ A product search feature contains a SQL injection vulnerability. Unlike basic authentication bypass, this challenge requires UNION-based injection to query a separate table.\n\nDatabase Structure:\n- products table: name, category, price, stock\n- secrets table: secret_key, secret_value (HIDDEN)\n\nObjective:\n1. Identify the number of columns in the products query\n2. Use UNION SELECT to query the secrets table\n3. Extract the admin_token value\n4. Submit it as the key\n\nRequired concepts:\n- UNION SQL operator\n- Column count matching\n- Cross-table data extraction
+ Vulnerable Code:\n\nString query = "SELECT name, category, price, stock FROM products WHERE name LIKE \'%" + input + "%\'";\n\nUNION-Based Injection allows:\n- Combining results from multiple queries\n- Accessing tables not in original query\n- Bypassing application logic\n- Extracting data from hidden tables\n\nAttack Requirements:\n1. Match column count in UNION\n2. Match data types (use NULL for unknowns)\n3. Terminate original query properly\n4. Know or guess target table name\n\nExample Attack:\n\' UNION SELECT secret_key, secret_value, NULL, NULL FROM secrets--
+ Product Search
+ Search the product database. Can you discover the hidden \'secrets\' table and extract the admin token?
+ Search products
+ Search results will appear here...
+ Once you\'ve extracted the admin token from the secrets table, submit it as the flag:
+
+
+ User Search
+ Find the hidden admin user!
+ Search username
+ Search Users
+
+
+ Username
+ Password
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/values/themes.xml b/mobile/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..5b385db38
--- /dev/null
+++ b/mobile/app/src/main/res/values/themes.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/xml/backup_rules.xml b/mobile/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 000000000..fa0f996d2
--- /dev/null
+++ b/mobile/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/xml/data_extraction_rules.xml b/mobile/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..9ee9997b0
--- /dev/null
+++ b/mobile/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/main/res/xml/file_paths.xml b/mobile/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 000000000..13b9f043f
--- /dev/null
+++ b/mobile/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/mobile/app/src/main/res/xml/preferences.xml b/mobile/app/src/main/res/xml/preferences.xml
new file mode 100644
index 000000000..5242c5eb0
--- /dev/null
+++ b/mobile/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/FlagValidatorTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/FlagValidatorTest.java
new file mode 100644
index 000000000..99459a8d4
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/FlagValidatorTest.java
@@ -0,0 +1,75 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.utils.FlagValidator;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for FlagValidator — verifies that offline flag values match their stored hashes.
+ *
+ * Each test uses the static {@link FlagValidator#validateFlag(FlagValidator.Module, String)}
+ * method so no Android context is required.
+ */
+public class FlagValidatorTest {
+
+ @Test
+ public void testReLessonOfflineFlag() {
+ assertTrue(FlagValidator.validateFlag(
+ FlagValidator.Module.RE_LESSON, "Frozen_Clock_Melts_By_Noon"));
+ }
+
+ @Test
+ public void testClientSideInjectionLessonOfflineFlag() {
+ assertTrue(FlagValidator.validateFlag(
+ FlagValidator.Module.CLIENT_SIDE_INJECTION_LESSON, "Marble_Rolls_Past_The_Cat"));
+ }
+
+ @Test
+ public void testInsecureAuthLessonOfflineFlag() {
+ assertTrue(FlagValidator.validateFlag(
+ FlagValidator.Module.INSECURE_AUTH_LESSON, "Iron_Gate_Opens_By_Silence"));
+ }
+
+ @Test
+ public void testInsecureCommLessonOfflineFlag() {
+ assertTrue(FlagValidator.validateFlag(
+ FlagValidator.Module.INSECURE_COMM_LESSON, "Signal_Lost_In_The_Fog"));
+ }
+
+ @Test
+ public void testPrivacyLessonOfflineFlag() {
+ assertTrue(FlagValidator.validateFlag(
+ FlagValidator.Module.PRIVACY_LESSON, "Photo_Leaks_GPS_Data"));
+ }
+
+ @Test
+ public void testInsufficientCryptoLessonOfflineFlag() {
+ assertTrue(FlagValidator.validateFlag(
+ FlagValidator.Module.INSUFFICIENT_CRYPTO_LESSON, "Weak_Key_Fails_The_Lock"));
+ }
+
+ @Test
+ public void testWrongFlagRejected() {
+ assertFalse(FlagValidator.validateFlag(
+ FlagValidator.Module.RE_LESSON, "wrong_flag"));
+ }
+
+ @Test
+ public void testEmptyFlagRejected() {
+ assertFalse(FlagValidator.validateFlag(
+ FlagValidator.Module.RE_LESSON, ""));
+ }
+
+ @Test
+ public void testNullFlagRejected() {
+ assertFalse(FlagValidator.validateFlag(
+ FlagValidator.Module.RE_LESSON, null));
+ }
+
+ @Test
+ public void testModuleWithNoHashReturnsFalse() {
+ // Modules that require server-side validation return false locally
+ assertFalse(FlagValidator.validateFlag(
+ FlagValidator.Module.POOR_AUTH_LESSON, "any_value"));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InputValidationLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InputValidationLessonTest.java
new file mode 100644
index 000000000..09928dd70
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InputValidationLessonTest.java
@@ -0,0 +1,45 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.InputValidationLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for InputValidationLessonFragment - testing deep link URL handling
+ */
+public class InputValidationLessonTest {
+
+ @Test
+ public void testAdminUrlConstantExists() throws Exception {
+ Field urlField = InputValidationLessonFragment.class.getDeclaredField("ADMIN_URL");
+ urlField.setAccessible(true);
+ String adminUrl = (String) urlField.get(null);
+ assertNotNull("ADMIN_URL constant should exist", adminUrl);
+ assertFalse("ADMIN_URL should not be empty", adminUrl.isEmpty());
+ }
+
+ @Test
+ public void testAdminUrlValue() throws Exception {
+ Field urlField = InputValidationLessonFragment.class.getDeclaredField("ADMIN_URL");
+ urlField.setAccessible(true);
+ String adminUrl = (String) urlField.get(null);
+ assertEquals("ADMIN_URL should match expected value",
+ "https://admin.internal/dashboard", adminUrl);
+ }
+
+ @Test
+ public void testAdminUrlIsHttps() throws Exception {
+ Field urlField = InputValidationLessonFragment.class.getDeclaredField("ADMIN_URL");
+ urlField.setAccessible(true);
+ String adminUrl = (String) urlField.get(null);
+ assertTrue("ADMIN_URL should be an HTTPS URL", adminUrl.startsWith("https://"));
+ }
+
+ @Test
+ public void testProcessDeepLinkMethodExists() throws Exception {
+ Method method = InputValidationLessonFragment.class.getDeclaredMethod("processDeepLink", String.class);
+ assertNotNull("processDeepLink method should exist", method);
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureAuthorizationLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureAuthorizationLessonTest.java
new file mode 100644
index 000000000..99bfbe396
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureAuthorizationLessonTest.java
@@ -0,0 +1,44 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.insecureauthorization.InsecureAuthorizationLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for InsecureAuthorizationLessonFragment - testing demo credentials and admin flag
+ */
+public class InsecureAuthorizationLessonTest {
+
+ @Test
+ public void testDemoUsernameExists() throws Exception {
+ Field usernameField = InsecureAuthorizationLessonFragment.class.getDeclaredField("DEMO_USERNAME");
+ usernameField.setAccessible(true);
+ String username = (String) usernameField.get(null);
+
+ assertNotNull("Demo username should exist", username);
+ assertFalse("Username should not be empty", username.isEmpty());
+ assertEquals("Username should be 'testuser'", "testuser", username);
+ }
+
+ @Test
+ public void testDemoPasswordExists() throws Exception {
+ Field passwordField = InsecureAuthorizationLessonFragment.class.getDeclaredField("DEMO_PASSWORD");
+ passwordField.setAccessible(true);
+ String password = (String) passwordField.get(null);
+
+ assertNotNull("Demo password should exist", password);
+ assertFalse("Password should not be empty", password.isEmpty());
+ assertEquals("Password should be 'password123'", "password123", password);
+ }
+
+ @Test
+ public void testPrefsNameConstant() throws Exception {
+ Field prefsField = InsecureAuthorizationLessonFragment.class.getDeclaredField("PREFS_NAME");
+ prefsField.setAccessible(true);
+ String prefsName = (String) prefsField.get(null);
+
+ assertEquals("SharedPreferences name should be 'UserSession'", "UserSession", prefsName);
+ }
+
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureCommChallengeTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureCommChallengeTest.java
new file mode 100644
index 000000000..eaa87282f
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureCommChallengeTest.java
@@ -0,0 +1,46 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.challenges.insecurecomm.InsecureCommChallengeFragment;
+import org.junit.Test;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for Insecure Communication Challenge
+ * Tests network security and SSL/TLS vulnerabilities
+ */
+public class InsecureCommChallengeTest {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("InsecureCommChallengeFragment class should exist",
+ InsecureCommChallengeFragment.class);
+ }
+
+ @Test
+ public void testOnCreateViewMethodExists() throws Exception {
+ Method method = InsecureCommChallengeFragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void testShowVulnerabilityInfoMethodExists() throws Exception {
+ Method method = InsecureCommChallengeFragment.class.getDeclaredMethod("showVulnerabilityInfo");
+ assertNotNull("showVulnerabilityInfo method should exist", method);
+ }
+
+ @Test
+ public void testOnDestroyViewMethodExists() throws Exception {
+ Method method = InsecureCommChallengeFragment.class.getMethod("onDestroyView");
+ assertNotNull("onDestroyView method should exist", method);
+ }
+
+ @Test
+ public void testFragmentExtendsFragment() {
+ assertTrue("Should extend Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(InsecureCommChallengeFragment.class));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureCommLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureCommLessonTest.java
new file mode 100644
index 000000000..c441b5696
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureCommLessonTest.java
@@ -0,0 +1,65 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.insecurecomm.InsecureCommLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for InsecureCommLessonFragment
+ */
+public class InsecureCommLessonTest {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("InsecureCommLessonFragment class should exist",
+ InsecureCommLessonFragment.class);
+ }
+
+ @Test
+ public void testTagConstantExists() throws Exception {
+ Field tagField = InsecureCommLessonFragment.class.getDeclaredField("TAG");
+ tagField.setAccessible(true);
+ String tag = (String) tagField.get(null);
+ assertNotNull("TAG constant should exist", tag);
+ assertFalse("TAG should not be empty", tag.isEmpty());
+ }
+
+ @Test
+ public void testTagValue() throws Exception {
+ Field tagField = InsecureCommLessonFragment.class.getDeclaredField("TAG");
+ tagField.setAccessible(true);
+ String tag = (String) tagField.get(null);
+ assertEquals("TAG should identify network traffic", "NetworkTraffic", tag);
+ }
+
+ @Test
+ public void testTagIsStaticFinal() throws Exception {
+ Field tagField = InsecureCommLessonFragment.class.getDeclaredField("TAG");
+ assertTrue("TAG should be static", Modifier.isStatic(tagField.getModifiers()));
+ assertTrue("TAG should be final", Modifier.isFinal(tagField.getModifiers()));
+ }
+
+ @Test
+ public void testFragmentExtendsAndroidXFragment() {
+ assertTrue("InsecureCommLessonFragment should extend androidx Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(InsecureCommLessonFragment.class));
+ }
+
+ @Test
+ public void testCurrentFlagFieldExists() throws Exception {
+ Field flagField = InsecureCommLessonFragment.class.getDeclaredField("currentFlag");
+ assertNotNull("currentFlag field should exist", flagField);
+ assertEquals("currentFlag should be of type String", String.class, flagField.getType());
+ assertFalse("currentFlag should be an instance field",
+ Modifier.isStatic(flagField.getModifiers()));
+ }
+
+ @Test
+ public void testCurrentFlagDefaultIsEmpty() throws Exception {
+ Field flagField = InsecureCommLessonFragment.class.getDeclaredField("currentFlag");
+ // Verify the initializer value in the class by checking the field type
+ assertEquals("currentFlag should be String type", String.class, flagField.getType());
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureData1Test.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureData1Test.java
new file mode 100644
index 000000000..b758e2e27
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureData1Test.java
@@ -0,0 +1,46 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.challenges.insecuredata1.InsecureData1Fragment;
+import org.junit.Test;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for Insecure Data Storage Challenge 1
+ * Tests SharedPreferences vulnerability
+ */
+public class InsecureData1Test {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("InsecureData1Fragment class should exist",
+ InsecureData1Fragment.class);
+ }
+
+ @Test
+ public void testOnCreateViewMethodExists() throws Exception {
+ Method method = InsecureData1Fragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void testShowVulnerabilityInfoMethodExists() throws Exception {
+ Method method = InsecureData1Fragment.class.getDeclaredMethod("showVulnerabilityInfo");
+ assertNotNull("showVulnerabilityInfo method should exist", method);
+ }
+
+ @Test
+ public void testOnDestroyViewMethodExists() throws Exception {
+ Method method = InsecureData1Fragment.class.getMethod("onDestroyView");
+ assertNotNull("onDestroyView method should exist", method);
+ }
+
+ @Test
+ public void testFragmentExtendsFragment() {
+ assertTrue("Should extend Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(InsecureData1Fragment.class));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureDataLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureDataLessonTest.java
new file mode 100644
index 000000000..8392db70f
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsecureDataLessonTest.java
@@ -0,0 +1,52 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.insecuredata.InsecureDataLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for InsecureDataLessonFragment
+ * This lesson doesn't have exposed constants but tests verify the class structure exists
+ */
+public class InsecureDataLessonTest {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("InsecureDataLessonFragment class should exist",
+ InsecureDataLessonFragment.class);
+ }
+
+ @Test
+ public void testCreateDatabaseMethodExists() throws Exception {
+ Method method = InsecureDataLessonFragment.class.getDeclaredMethod("createDatabase");
+ assertNotNull("createDatabase method should exist", method);
+ }
+
+ @Test
+ public void testInsertUsersMethodExists() throws Exception {
+ Method method = InsecureDataLessonFragment.class.getDeclaredMethod("insertUsers");
+ assertNotNull("insertUsers method should exist", method);
+ }
+
+ @Test
+ public void testShowDetailedInfoMethodExists() throws Exception {
+ Method method = InsecureDataLessonFragment.class.getDeclaredMethod("showDetailedInfo");
+ assertNotNull("showDetailedInfo method should exist", method);
+ }
+
+ @Test
+ public void testFragmentHasOnCreateViewMethod() throws Exception {
+ Method method = InsecureDataLessonFragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void testFragmentHasOnDestroyViewMethod() throws Exception {
+ Method method = InsecureDataLessonFragment.class.getMethod("onDestroyView");
+ assertNotNull("onDestroyView method should exist", method);
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InsufficientCryptoChallengeTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsufficientCryptoChallengeTest.java
new file mode 100644
index 000000000..dbb389aec
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsufficientCryptoChallengeTest.java
@@ -0,0 +1,46 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.challenges.crypto.InsufficientCryptoChallengeFragment;
+import org.junit.Test;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for Insufficient Cryptography Challenge
+ * Tests weak cryptographic implementations
+ */
+public class InsufficientCryptoChallengeTest {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("InsufficientCryptoChallengeFragment class should exist",
+ InsufficientCryptoChallengeFragment.class);
+ }
+
+ @Test
+ public void testOnCreateViewMethodExists() throws Exception {
+ Method method = InsufficientCryptoChallengeFragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void testShowVulnerabilityInfoMethodExists() throws Exception {
+ Method method = InsufficientCryptoChallengeFragment.class.getDeclaredMethod("showVulnerabilityInfo");
+ assertNotNull("showVulnerabilityInfo method should exist", method);
+ }
+
+ @Test
+ public void testOnDestroyViewMethodExists() throws Exception {
+ Method method = InsufficientCryptoChallengeFragment.class.getMethod("onDestroyView");
+ assertNotNull("onDestroyView method should exist", method);
+ }
+
+ @Test
+ public void testFragmentExtendsFragment() {
+ assertTrue("Should extend Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(InsufficientCryptoChallengeFragment.class));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/InsufficientCryptoLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsufficientCryptoLessonTest.java
new file mode 100644
index 000000000..1db3a6466
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/InsufficientCryptoLessonTest.java
@@ -0,0 +1,37 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.crypto.InsufficientCryptoLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for InsufficientCryptoLessonFragment
+ */
+public class InsufficientCryptoLessonTest {
+
+ @Test
+ public void testAlgorithmConstantExists() throws Exception {
+ Field algoField = InsufficientCryptoLessonFragment.class.getDeclaredField("ALGORITHM");
+ algoField.setAccessible(true);
+ String algorithm = (String) algoField.get(null);
+ assertNotNull("ALGORITHM constant should exist", algorithm);
+ assertFalse("ALGORITHM should not be empty", algorithm.isEmpty());
+ }
+
+ @Test
+ public void testAlgorithmIsDes() throws Exception {
+ Field algoField = InsufficientCryptoLessonFragment.class.getDeclaredField("ALGORITHM");
+ algoField.setAccessible(true);
+ String algorithm = (String) algoField.get(null);
+ assertEquals("Algorithm should be DES (weak 56-bit cipher)", "DES", algorithm);
+ }
+
+ @Test
+ public void testAlgorithmIsStaticFinal() throws Exception {
+ Field algoField = InsufficientCryptoLessonFragment.class.getDeclaredField("ALGORITHM");
+ assertTrue("ALGORITHM should be static", Modifier.isStatic(algoField.getModifiers()));
+ assertTrue("ALGORITHM should be final", Modifier.isFinal(algoField.getModifiers()));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/LessonFragmentTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/LessonFragmentTest.java
new file mode 100644
index 000000000..8041f60f3
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/LessonFragmentTest.java
@@ -0,0 +1,38 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.LessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for LessonFragment
+ */
+public class LessonFragmentTest {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("LessonFragment class should exist", LessonFragment.class);
+ }
+
+ @Test
+ public void testFragmentExtendsAndroidXFragment() {
+ assertTrue("LessonFragment should extend androidx Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(LessonFragment.class));
+ }
+
+ @Test
+ public void testCurrentFlagFieldExists() throws Exception {
+ Field flagField = LessonFragment.class.getDeclaredField("currentFlag");
+ assertNotNull("currentFlag field should exist", flagField);
+ assertEquals("currentFlag should be of type String", String.class, flagField.getType());
+ }
+
+ @Test
+ public void testCurrentFlagIsInstanceField() throws Exception {
+ Field flagField = LessonFragment.class.getDeclaredField("currentFlag");
+ assertFalse("currentFlag should be an instance field, not static",
+ Modifier.isStatic(flagField.getModifiers()));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/PoorAuthChallengeTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/PoorAuthChallengeTest.java
new file mode 100644
index 000000000..53719b359
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/PoorAuthChallengeTest.java
@@ -0,0 +1,46 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.challenges.poorauth.PoorAuthChallengeFragment;
+import org.junit.Test;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for Poor Authentication Challenge
+ * Tests weak authentication implementation
+ */
+public class PoorAuthChallengeTest {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("PoorAuthChallengeFragment class should exist",
+ PoorAuthChallengeFragment.class);
+ }
+
+ @Test
+ public void testOnCreateViewMethodExists() throws Exception {
+ Method method = PoorAuthChallengeFragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void testShowVulnerabilityInfoMethodExists() throws Exception {
+ Method method = PoorAuthChallengeFragment.class.getDeclaredMethod("showVulnerabilityInfo");
+ assertNotNull("showVulnerabilityInfo method should exist", method);
+ }
+
+ @Test
+ public void testOnDestroyViewMethodExists() throws Exception {
+ Method method = PoorAuthChallengeFragment.class.getMethod("onDestroyView");
+ assertNotNull("onDestroyView method should exist", method);
+ }
+
+ @Test
+ public void testFragmentExtendsFragment() {
+ assertTrue("Should extend Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(PoorAuthChallengeFragment.class));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/PoorAuthLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/PoorAuthLessonTest.java
new file mode 100644
index 000000000..fb9c3d775
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/PoorAuthLessonTest.java
@@ -0,0 +1,57 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.poorauth.PoorAuthLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for PoorAuthLessonFragment
+ * The fragment stores a hashed PIN and an obfuscated flag (XOR + Base64 encoded).
+ */
+public class PoorAuthLessonTest {
+
+ @Test
+ public void testHardcodedPinHashExists() throws Exception {
+ Field hashField = PoorAuthLessonFragment.class.getDeclaredField("HARDCODED_PIN_HASH");
+ hashField.setAccessible(true);
+ String hash = (String) hashField.get(null);
+ assertNotNull("HARDCODED_PIN_HASH should exist", hash);
+ assertFalse("Hash should not be empty", hash.isEmpty());
+ }
+
+ @Test
+ public void testHardcodedPinHashIsHex() throws Exception {
+ Field hashField = PoorAuthLessonFragment.class.getDeclaredField("HARDCODED_PIN_HASH");
+ hashField.setAccessible(true);
+ String hash = (String) hashField.get(null);
+ assertTrue("Hash should be a lowercase hex string", hash.matches("[0-9a-f]+"));
+ }
+
+ @Test
+ public void testHardcodedPinHashIsSha256Length() throws Exception {
+ Field hashField = PoorAuthLessonFragment.class.getDeclaredField("HARDCODED_PIN_HASH");
+ hashField.setAccessible(true);
+ String hash = (String) hashField.get(null);
+ assertEquals("SHA-256 hash should be 64 hex characters", 64, hash.length());
+ }
+
+ @Test
+ public void testObfuscatedFlagArrayExists() throws Exception {
+ Field fField = PoorAuthLessonFragment.class.getDeclaredField("F");
+ fField.setAccessible(true);
+ Object value = fField.get(null);
+ assertNotNull("Obfuscated flag array F should exist", value);
+ assertTrue("F should be a String array", value instanceof String[]);
+ assertTrue("F array should not be empty", ((String[]) value).length > 0);
+ }
+
+ @Test
+ public void testXorKeyExists() throws Exception {
+ Field kField = PoorAuthLessonFragment.class.getDeclaredField("K");
+ assertEquals("K field should be of type byte", byte.class, kField.getType());
+ assertTrue("K should be static", Modifier.isStatic(kField.getModifiers()));
+ assertTrue("K should be final", Modifier.isFinal(kField.getModifiers()));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/PrivacyControlsLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/PrivacyControlsLessonTest.java
new file mode 100644
index 000000000..7f8b04847
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/PrivacyControlsLessonTest.java
@@ -0,0 +1,79 @@
+package org.owasp.mobileshepherd;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.lang.reflect.Field;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for Privacy Controls Lesson (M6: Inadequate Privacy Controls)
+ * Tests metadata handling and EXIF data functionality
+ */
+@RunWith(JUnit4.class)
+public class PrivacyControlsLessonTest {
+
+ @Test
+ public void testFragmentClassExists() throws ClassNotFoundException {
+ // Verify the fragment class exists
+ Class> fragmentClass = Class.forName("org.owasp.mobileshepherd.ui.lessons.privacycontrols.PrivacyControlsLessonFragment");
+ assertNotNull("PrivacyControlsLessonFragment class should exist", fragmentClass);
+ }
+
+ @Test
+ public void testFragmentExtendsFragment() throws ClassNotFoundException {
+ Class> fragmentClass = Class.forName("org.owasp.mobileshepherd.ui.lessons.privacycontrols.PrivacyControlsLessonFragment");
+
+ // Check if it extends Fragment
+ boolean extendsFragment = false;
+ Class> superClass = fragmentClass.getSuperclass();
+ while (superClass != null) {
+ if (superClass.getSimpleName().equals("Fragment")) {
+ extendsFragment = true;
+ break;
+ }
+ superClass = superClass.getSuperclass();
+ }
+
+ assertTrue("PrivacyControlsLessonFragment should extend Fragment", extendsFragment);
+ }
+
+ @Test
+ public void testPreloadedImageConstantExists() throws Exception {
+ Class> fragmentClass = Class.forName("org.owasp.mobileshepherd.ui.lessons.privacycontrols.PrivacyControlsLessonFragment");
+
+ Field preloadedImageField = fragmentClass.getDeclaredField("PRELOADED_IMAGE");
+ preloadedImageField.setAccessible(true);
+ String preloadedImageValue = (String) preloadedImageField.get(null);
+
+ assertNotNull("PRELOADED_IMAGE constant should not be null", preloadedImageValue);
+ assertTrue("PRELOADED_IMAGE should be a valid filename", preloadedImageValue.endsWith(".jpg") || preloadedImageValue.endsWith(".jpeg"));
+ }
+
+ @Test
+ public void testShowDetailedInfoMethodExists() throws Exception {
+ Class> fragmentClass = Class.forName("org.owasp.mobileshepherd.ui.lessons.privacycontrols.PrivacyControlsLessonFragment");
+
+ try {
+ fragmentClass.getDeclaredMethod("showDetailedInfo");
+ } catch (NoSuchMethodException e) {
+ fail("showDetailedInfo() method should exist for FAB functionality");
+ }
+ }
+
+ @Test
+ public void testRequiredMethodsExist() throws Exception {
+ Class> fragmentClass = Class.forName("org.owasp.mobileshepherd.ui.lessons.privacycontrols.PrivacyControlsLessonFragment");
+
+ // Check for key methods
+ assertNotNull("loadPreloadedImage method should exist",
+ fragmentClass.getDeclaredMethod("loadPreloadedImage"));
+ assertNotNull("analyzeCurrentImage method should exist",
+ fragmentClass.getDeclaredMethod("analyzeCurrentImage"));
+ assertNotNull("createPreloadedImageWithFlag method should exist",
+ fragmentClass.getDeclaredMethod("createPreloadedImageWithFlag", File.class));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/ReverseEngineeringChallenge1Test.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/ReverseEngineeringChallenge1Test.java
new file mode 100644
index 000000000..2280bc7b0
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/ReverseEngineeringChallenge1Test.java
@@ -0,0 +1,74 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.challenges.reverseengineering.ReverseEngineering1Model;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for ReverseEngineering1Model
+ * The model stores an encoded secret (Base64) that can be discovered via reverse engineering.
+ */
+public class ReverseEngineeringChallenge1Test {
+
+ @Test
+ public void challenge1_EncodedSecretIsAccessibleViaReflection() throws Exception {
+ Field secretField = ReverseEngineering1Model.class.getDeclaredField("ENCODED_SECRET");
+ secretField.setAccessible(true);
+ String encodedSecret = (String) secretField.get(null);
+ assertNotNull("ENCODED_SECRET should exist", encodedSecret);
+ assertFalse("ENCODED_SECRET should not be empty", encodedSecret.isEmpty());
+ }
+
+ @Test
+ public void challenge1_ValidateFlagMethodExists() throws Exception {
+ Method validateMethod = ReverseEngineering1Model.class.getDeclaredMethod("validateFlag", String.class);
+ assertNotNull("validateFlag method should exist", validateMethod);
+ }
+
+ @Test
+ public void challenge1_EncodedSecretIsStaticFinal() throws Exception {
+ Field secretField = ReverseEngineering1Model.class.getDeclaredField("ENCODED_SECRET");
+ assertTrue("ENCODED_SECRET should be static", Modifier.isStatic(secretField.getModifiers()));
+ assertTrue("ENCODED_SECRET should be final", Modifier.isFinal(secretField.getModifiers()));
+ }
+
+ @Test
+ public void challenge1_EncodedSecretIsValidBase64() throws Exception {
+ Field secretField = ReverseEngineering1Model.class.getDeclaredField("ENCODED_SECRET");
+ secretField.setAccessible(true);
+ String encodedSecret = (String) secretField.get(null);
+ // Valid Base64 characters: A-Z, a-z, 0-9, +, /, =
+ assertTrue("ENCODED_SECRET should be valid Base64",
+ encodedSecret.matches("^[A-Za-z0-9+/]+=*$"));
+ }
+
+ @Test
+ public void challenge1_DiscoverEncodedSecretThroughFieldEnumeration() {
+ try {
+ Field[] fields = ReverseEngineering1Model.class.getDeclaredFields();
+ boolean foundEncodedField = false;
+
+ for (Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers()) && field.getType() == String.class) {
+ field.setAccessible(true);
+ Object value = field.get(null);
+ if (value instanceof String) {
+ String str = (String) value;
+ // Encoded secrets end with = padding or match Base64 pattern
+ if (str.endsWith("=") || str.matches("^[A-Za-z0-9+/]+=*$")) {
+ foundEncodedField = true;
+ break;
+ }
+ }
+ }
+ }
+
+ assertTrue("Should discover an encoded field through field enumeration", foundEncodedField);
+ } catch (Exception e) {
+ fail("Should be able to enumerate fields: " + e.getMessage());
+ }
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/ReverseEngineeringLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/ReverseEngineeringLessonTest.java
new file mode 100644
index 000000000..73a7b9c45
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/ReverseEngineeringLessonTest.java
@@ -0,0 +1,57 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.LessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for LessonFragment (Reverse Engineering Lesson)
+ */
+public class ReverseEngineeringLessonTest {
+
+ @Test
+ public void lesson_ClassExists() {
+ assertNotNull("LessonFragment class should exist", LessonFragment.class);
+ }
+
+ @Test
+ public void lesson_FragmentExtendsAndroidXFragment() {
+ assertTrue("LessonFragment should extend androidx Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(LessonFragment.class));
+ }
+
+ @Test
+ public void lesson_HasCurrentFlagField() throws Exception {
+ Field flagField = LessonFragment.class.getDeclaredField("currentFlag");
+ assertEquals("currentFlag should be of type String", String.class, flagField.getType());
+ assertFalse("currentFlag should be an instance field",
+ Modifier.isStatic(flagField.getModifiers()));
+ }
+
+ @Test
+ public void lesson_HasBindingField() throws Exception {
+ Field bindingField = LessonFragment.class.getDeclaredField("binding");
+ assertNotNull("binding field should exist", bindingField);
+ assertFalse("binding should be an instance field",
+ Modifier.isStatic(bindingField.getModifiers()));
+ }
+
+ @Test
+ public void lesson_OnCreateViewMethodExists() throws Exception {
+ Method method = LessonFragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void lesson_HasAtLeastTwoFields() {
+ Field[] fields = LessonFragment.class.getDeclaredFields();
+ assertTrue("LessonFragment should have at least 2 declared fields",
+ fields.length >= 2);
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/SecurityMisconfigChallenge2Test.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/SecurityMisconfigChallenge2Test.java
new file mode 100644
index 000000000..e29296940
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/SecurityMisconfigChallenge2Test.java
@@ -0,0 +1,46 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.challenges.securitymisconfig.SecurityMisconfigChallenge2Fragment;
+import org.junit.Test;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for Security Misconfiguration Challenge 2
+ * Tests security configuration vulnerabilities
+ */
+public class SecurityMisconfigChallenge2Test {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("SecurityMisconfigChallenge2Fragment class should exist",
+ SecurityMisconfigChallenge2Fragment.class);
+ }
+
+ @Test
+ public void testOnCreateViewMethodExists() throws Exception {
+ Method method = SecurityMisconfigChallenge2Fragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void testShowVulnerabilityInfoMethodExists() throws Exception {
+ Method method = SecurityMisconfigChallenge2Fragment.class.getDeclaredMethod("showVulnerabilityInfo");
+ assertNotNull("showVulnerabilityInfo method should exist", method);
+ }
+
+ @Test
+ public void testOnDestroyViewMethodExists() throws Exception {
+ Method method = SecurityMisconfigChallenge2Fragment.class.getMethod("onDestroyView");
+ assertNotNull("onDestroyView method should exist", method);
+ }
+
+ @Test
+ public void testFragmentExtendsFragment() {
+ assertTrue("Should extend Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(SecurityMisconfigChallenge2Fragment.class));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/SecurityMisconfigLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/SecurityMisconfigLessonTest.java
new file mode 100644
index 000000000..c6daeca86
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/SecurityMisconfigLessonTest.java
@@ -0,0 +1,57 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.securitymisconfig.SecurityMisconfigLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for SecurityMisconfigLessonFragment
+ * Tests verify class structure and TAG constant
+ */
+public class SecurityMisconfigLessonTest {
+
+ @Test
+ public void testClassExists() {
+ assertNotNull("SecurityMisconfigLessonFragment class should exist",
+ SecurityMisconfigLessonFragment.class);
+ }
+
+ @Test
+ public void testTagConstantExists() throws Exception {
+ Field tagField = SecurityMisconfigLessonFragment.class.getDeclaredField("TAG");
+ tagField.setAccessible(true);
+ String tag = (String) tagField.get(null);
+
+ assertNotNull("TAG should exist", tag);
+ assertEquals("TAG should be 'SecurityMisconfig'", "SecurityMisconfig", tag);
+ }
+
+ @Test
+ public void testCheckConfigurationMethodExists() throws Exception {
+ Method method = SecurityMisconfigLessonFragment.class.getDeclaredMethod("checkConfiguration");
+ assertNotNull("checkConfiguration method should exist", method);
+ }
+
+ @Test
+ public void testShowDetailedInfoMethodExists() throws Exception {
+ Method method = SecurityMisconfigLessonFragment.class.getDeclaredMethod("showDetailedInfo");
+ assertNotNull("showDetailedInfo method should exist", method);
+ }
+
+ @Test
+ public void testFragmentHasOnCreateViewMethod() throws Exception {
+ Method method = SecurityMisconfigLessonFragment.class.getMethod("onCreateView",
+ android.view.LayoutInflater.class,
+ android.view.ViewGroup.class,
+ android.os.Bundle.class);
+ assertNotNull("onCreateView method should exist", method);
+ }
+
+ @Test
+ public void testFragmentExtendsFragment() {
+ assertTrue("SecurityMisconfigLessonFragment should extend Fragment",
+ androidx.fragment.app.Fragment.class.isAssignableFrom(SecurityMisconfigLessonFragment.class));
+ }
+}
diff --git a/mobile/app/src/test/java/org/owasp/mobileshepherd/SupplyChainLessonTest.java b/mobile/app/src/test/java/org/owasp/mobileshepherd/SupplyChainLessonTest.java
new file mode 100644
index 000000000..f781d7558
--- /dev/null
+++ b/mobile/app/src/test/java/org/owasp/mobileshepherd/SupplyChainLessonTest.java
@@ -0,0 +1,67 @@
+package org.owasp.mobileshepherd;
+
+import org.owasp.mobileshepherd.ui.lessons.supplychain.SupplyChainLessonFragment;
+import org.junit.Test;
+import java.lang.reflect.Field;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for SupplyChainLessonFragment
+ * The fragment embeds a vulnerable library identifier and an EXIF debug key.
+ */
+public class SupplyChainLessonTest {
+
+ @Test
+ public void testVulnerableLibConstantExists() throws Exception {
+ Field libField = SupplyChainLessonFragment.class.getDeclaredField("VULNERABLE_LIB");
+ libField.setAccessible(true);
+ String lib = (String) libField.get(null);
+ assertNotNull("VULNERABLE_LIB constant should exist", lib);
+ assertFalse("VULNERABLE_LIB should not be empty", lib.isEmpty());
+ }
+
+ @Test
+ public void testVulnerableLibValue() throws Exception {
+ Field libField = SupplyChainLessonFragment.class.getDeclaredField("VULNERABLE_LIB");
+ libField.setAccessible(true);
+ String lib = (String) libField.get(null);
+ assertEquals("Vulnerable library should match expected value",
+ "androidx.exifinterface:exifinterface:1.3.7", lib);
+ }
+
+ @Test
+ public void testVulnerableLibIsMavenCoordinate() throws Exception {
+ Field libField = SupplyChainLessonFragment.class.getDeclaredField("VULNERABLE_LIB");
+ libField.setAccessible(true);
+ String lib = (String) libField.get(null);
+ assertTrue("VULNERABLE_LIB should be a Maven coordinate (group:artifact:version)",
+ lib.contains(":") && lib.split(":").length == 3);
+ }
+
+ @Test
+ public void testExifDebugKeyExists() throws Exception {
+ Field keyField = SupplyChainLessonFragment.class.getDeclaredField("EXIF_DEBUG_KEY");
+ keyField.setAccessible(true);
+ String key = (String) keyField.get(null);
+ assertNotNull("EXIF_DEBUG_KEY constant should exist", key);
+ assertFalse("EXIF_DEBUG_KEY should not be empty", key.isEmpty());
+ }
+
+ @Test
+ public void testExifDebugKeyValue() throws Exception {
+ Field keyField = SupplyChainLessonFragment.class.getDeclaredField("EXIF_DEBUG_KEY");
+ keyField.setAccessible(true);
+ String key = (String) keyField.get(null);
+ assertEquals("EXIF debug key should match expected value",
+ "exif_debug_processor_key_1337", key);
+ }
+
+ @Test
+ public void testExifDebugKeyContainsDebugIndicator() throws Exception {
+ Field keyField = SupplyChainLessonFragment.class.getDeclaredField("EXIF_DEBUG_KEY");
+ keyField.setAccessible(true);
+ String key = (String) keyField.get(null);
+ assertTrue("Key should contain 'debug' indicating it should not be in production",
+ key.contains("debug"));
+ }
+}
diff --git a/mobile/build.gradle b/mobile/build.gradle
new file mode 100644
index 000000000..6bd28ec41
--- /dev/null
+++ b/mobile/build.gradle
@@ -0,0 +1,9 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '8.2.2' apply false
+ id 'com.android.library' version '8.2.2' apply false
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/mobile/gradle.properties b/mobile/gradle.properties
new file mode 100644
index 000000000..a9f695da5
--- /dev/null
+++ b/mobile/gradle.properties
@@ -0,0 +1,25 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1024m -Dfile.encoding=UTF-8
+# Configure Gradle to use Java 17
+org.gradle.java.home=C:\\Program Files\\Android\\Android Studio\\jbr
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+# Keep R class fields as final to support switch statements (AGP 8.x compatibility)
+android.nonFinalResIds=false
\ No newline at end of file
diff --git a/mobile/gradle/wrapper/gradle-wrapper.properties b/mobile/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..91df2e4b9
--- /dev/null
+++ b/mobile/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Jun 30 17:15:17 BST 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/mobile/gradlew b/mobile/gradlew
new file mode 100644
index 000000000..4f906e0c8
--- /dev/null
+++ b/mobile/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/mobile/gradlew.bat b/mobile/gradlew.bat
new file mode 100644
index 000000000..107acd32c
--- /dev/null
+++ b/mobile/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/mobile/scripts/calculate_key_hashes.py b/mobile/scripts/calculate_key_hashes.py
new file mode 100644
index 000000000..0c4157a7b
--- /dev/null
+++ b/mobile/scripts/calculate_key_hashes.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""
+Calculate SHA-256 hashes for all KEY{} format flags.
+Converts from OWASP{} to KEY{} format and generates new hashes for FlagValidator.java
+"""
+
+import hashlib
+
+def sha256_hash(text):
+ """Calculate SHA-256 hash of text"""
+ return hashlib.sha256(text.encode('utf-8')).hexdigest()
+
+# All flags in KEY{} format (converted from OWASP{})
+flags = {
+ # Lessons (11)
+ "RE_LESSON": "KEY{R3v3rs3_Eng1n33r1ng_M4st3r_2024}",
+ "IDS_LESSON": "KEY{1ns3cur3_D4t4_St0r4g3_L34k}",
+ "POOR_AUTH_LESSON": "KEY{T4co_Sn0r3s_0n_4_C0uch}",
+ "INSECURE_AUTH_LESSON": "KEY{Pr1v1l3g3_Esc4l4t10n_Pwn3d}",
+ "SUPPLY_CHAIN_LESSON": "KEY{Vuln3r4bl3_D3p3nd3ncy}",
+ "INSECURE_COMM_LESSON": "KEY{Unsecur3_HTTP_Tr4ff1c}",
+ "INSUFFICIENT_CRYPTO_LESSON": "KEY{W3ak_DES_Encrypt10n}",
+ "SECURITY_MISCONFIG_LESSON": "KEY{D3bugg4bl3_Fl4g_F0und}",
+ "INPUT_VALIDATION_LESSON": "KEY{1nput_V4l1d4t10n_Byp4ss3d}",
+ "PRIVACY_LESSON": "KEY{3x1f_M3t4d4t4_L34k5_L0c4t10n}",
+ "CLIENT_SIDE_INJECTION_LESSON": "KEY{CL13NT_S1D3_SQL_1NJ3CT10N}",
+
+ # Challenges (10)
+ "RE_CHALLENGE_1": "KEY{S1mpl3_Fl4g_34sy_T0_F1nd}",
+ "IDS_CHALLENGE_1": "KEY{SQLit3_D4t4_3xtr4ct3d}",
+ "POOR_AUTH_CHALLENGE": "KEY{P00r_Auth_W34k_Qu3st10ns}",
+ "INSECURE_COMM_CHALLENGE": "KEY{N3tw0rk_Sn1ff3d}",
+ "INSUFFICIENT_CRYPTO_CHALLENGE": "KEY{ECB_M0d3_Vuln3r4bl3}",
+ "SECURITY_MISCONFIG_CHALLENGE_2": "KEY{B4ckup_D4t4_3xtr4ct3d}",
+ "CLIENT_SIDE_INJECTION_CHALLENGE_1": "KEY{SQL_1nj3ct10n_4dm1n_Pwn}",
+ "CLIENT_SIDE_INJECTION_CHALLENGE_2": "KEY{UN10N_B4s3d_1nj3ct10n}",
+}
+
+print("=" * 80)
+print("KEY{} FLAG SHA-256 HASHES")
+print("=" * 80)
+print()
+
+print("// Lessons (11)")
+for module in ["RE_LESSON", "IDS_LESSON", "POOR_AUTH_LESSON", "INSECURE_AUTH_LESSON",
+ "SUPPLY_CHAIN_LESSON", "INSECURE_COMM_LESSON", "INSUFFICIENT_CRYPTO_LESSON",
+ "SECURITY_MISCONFIG_LESSON", "INPUT_VALIDATION_LESSON", "PRIVACY_LESSON",
+ "CLIENT_SIDE_INJECTION_LESSON"]:
+ flag = flags[module]
+ hash_value = sha256_hash(flag)
+ print(f'put(Module.{module}, "{hash_value}");')
+
+print()
+print("// Challenges (9)")
+for module in ["RE_CHALLENGE_1",
+ "IDS_CHALLENGE_1",
+ "POOR_AUTH_CHALLENGE",
+ "INSECURE_COMM_CHALLENGE", "INSUFFICIENT_CRYPTO_CHALLENGE",
+ "SECURITY_MISCONFIG_CHALLENGE_2",
+ "CLIENT_SIDE_INJECTION_CHALLENGE_1", "CLIENT_SIDE_INJECTION_CHALLENGE_2"]:
+ flag = flags[module]
+ hash_value = sha256_hash(flag)
+ print(f'put(Module.{module}, "{hash_value}");')
+
+print()
+print("=" * 80)
+print("VERIFICATION TABLE")
+print("=" * 80)
+print(f"{'Module':<40} {'Flag':<50}")
+print("-" * 90)
+for module, flag in flags.items():
+ print(f"{module:<40} {flag:<50}")
diff --git a/mobile/settings.gradle b/mobile/settings.gradle
new file mode 100644
index 000000000..1933f897a
--- /dev/null
+++ b/mobile/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "reverser"
+include ':app'
diff --git a/src/it/java/dbProcs/GetterIT.java b/src/it/java/dbProcs/GetterIT.java
index 3aac6a7e9..295b04b87 100644
--- a/src/it/java/dbProcs/GetterIT.java
+++ b/src/it/java/dbProcs/GetterIT.java
@@ -37,7 +37,7 @@ public class GetterIT {
private static String lang = "en_GB";
private static Locale locale = new Locale(lang);
private static String applicationRoot = new String();
- private static final int totalNumberOfModulesInShepherd = 58;
+ private static final int totalNumberOfModulesInShepherd = 54;
/** Creates DB or Restores DB to Factory Defaults before running tests */
@BeforeAll
@@ -614,7 +614,7 @@ public void testCheckPlayerResultWhenModuleComplete() {
@Test
public void testCheckPlayerResultWhenModuleNotComplete() {
String userName = new String("userHasModulesOpened");
- String contentProviderLeakage = new String("5b461ebe2e5e2797740cb3e9c7e3f93449a93e3a");
+ String clientSideInjectionLesson = new String("335440fef02d19259254ed88293b62f31cccdd41");
try {
if (verifyTestUser(applicationRoot, userName, userName)) {
if (Setter.openAllModules(applicationRoot, false)
@@ -622,13 +622,13 @@ public void testCheckPlayerResultWhenModuleNotComplete() {
// Simulate user Opening Level
if (!Getter.getModuleAddress(
applicationRoot,
- contentProviderLeakage,
+ clientSideInjectionLesson,
Getter.getUserIdFromName(applicationRoot, userName))
.isEmpty()) {
String checkPlayerResultTest =
Getter.checkPlayerResult(
applicationRoot,
- contentProviderLeakage,
+ clientSideInjectionLesson,
Getter.getUserIdFromName(applicationRoot, userName));
if (checkPlayerResultTest != null) {
return; // Pass
@@ -636,7 +636,7 @@ public void testCheckPlayerResultWhenModuleNotComplete() {
fail("Function says user has not opened challenge or has completed challenge before");
}
} else {
- fail("Could not Content Provider Leakage Lesson as Opened by user");
+ fail("Could not open Client Side Injection Lesson for user");
}
} else {
fail("Could not Mark Modules As Opened");
diff --git a/src/main/java/dbProcs/Setter.java b/src/main/java/dbProcs/Setter.java
index ff1fd9177..d32f47303 100644
--- a/src/main/java/dbProcs/Setter.java
+++ b/src/main/java/dbProcs/Setter.java
@@ -55,14 +55,17 @@ public class Setter {
public static final String mobileModuleCategoryHardcodedWhereClause =
new String(
""
- + "moduleCategory = 'Mobile Broken Crypto'"
- + " OR moduleCategory = 'Mobile Content Provider'"
- + " OR moduleCategory = 'Mobile Data Leakage'"
- + " OR moduleCategory = 'Mobile Injection'"
+ + "moduleCategory = 'Mobile Injection'"
+ + " OR moduleCategory = 'Mobile Insecure Authorization'"
+ + " OR moduleCategory = 'Mobile Insecure Communication'"
+ " OR moduleCategory = 'Mobile Insecure Data Storage'"
+ + " OR moduleCategory = 'Mobile Input Validation'"
+ + " OR moduleCategory = 'Mobile Insufficient Cryptography'"
+ " OR moduleCategory = 'Mobile Poor Authentication'"
+ + " OR moduleCategory = 'Mobile Privacy Controls'"
+ " OR moduleCategory = 'Mobile Reverse Engineering'"
- + " OR moduleCategory = 'Mobile Security Decisions via Untrusted Input'");
+ + " OR moduleCategory = 'Mobile Security Misconfiguration'"
+ + " OR moduleCategory = 'Mobile Supply Chain'");
/**
* Database procedure just adds this. So this method just prepares the statement
diff --git a/src/main/java/servlets/MobileFlagGet.java b/src/main/java/servlets/MobileFlagGet.java
new file mode 100644
index 000000000..d6660ab4f
--- /dev/null
+++ b/src/main/java/servlets/MobileFlagGet.java
@@ -0,0 +1,139 @@
+package servlets;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.json.JSONObject;
+import utils.Hash;
+import utils.ShepherdLogManager;
+
+/**
+ * Returns a user-specific dynamic flag for the requested mobile module.
+ *
+ * The flag is the HMAC-SHA512 of the module's base flag keyed with the server's ephemeral key and
+ * salted with the authenticated user's name — matching exactly what {@link MobileFlagSubmit} will
+ * recompute during validation. This means:
+ *
+ *
+ *
Each student receives a different flag string.
+ *
Flags cannot be shared between students.
+ *
The base flag value is never sent to the client.
+ *
Authentication via the Shepherd session cookie (JSESSIONID) set during mobile login.
+ *
+ *
Response (application/json):
+ *
+ *
+ * {"flag":"<hmac-hex>"} – authenticated, module known
+ * {"error":"..."} – authentication failure or unknown module
+ *
+ *
+ *
This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ *
+ * @author Sean Duggan
+ */
+public class MobileFlagGet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger log = LogManager.getLogger(MobileFlagGet.class);
+
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
+ log.debug("**** servlets.MobileFlagGet ****");
+
+ response.setCharacterEncoding("UTF-8");
+ request.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+
+ PrintWriter out = response.getWriter();
+
+ String moduleId = request.getParameter("moduleId");
+
+ if (moduleId == null) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ out.write(errorJson("Missing required parameters"));
+ return;
+ }
+
+ HttpSession ses = request.getSession(false);
+ if (ses == null
+ || ses.getAttribute("userName") == null
+ || ses.getAttribute("userStamp") == null) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ out.write(errorJson("Not authenticated"));
+ return;
+ }
+
+ String userId = (String) ses.getAttribute("userStamp");
+ String userName = (String) ses.getAttribute("userName");
+
+ String baseFlag = MobileModuleFlags.BASE_FLAGS.get(moduleId);
+ if (baseFlag == null) {
+ log.debug("Unknown mobile module ID: " + MobileModuleFlags.sanitize(moduleId));
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ out.write(errorJson("Module not found"));
+ return;
+ }
+
+ // Gate: only deliver a flag if the student has opened this module via the app.
+ // This prevents bulk flag farming by calling this endpoint directly after login.
+ boolean started = MobileModuleProgress.hasStarted(userId, moduleId);
+ if (!started) {
+ log.debug(
+ "Flag requested for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " by "
+ + MobileModuleFlags.sanitize(userName)
+ + " but module not started");
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ out.write(errorJson("Module not started — open it in the app first"));
+ return;
+ }
+
+ // Derive a user-specific flag. Hash.generateUserSolutionKeyOnly uses HmacSHA512
+ // keyed with the server's ephemeral key — same computation as MobileFlagSubmit.
+ String dynamicFlag = Hash.generateUserSolutionKeyOnly(baseFlag, userName);
+ log.debug(
+ "Returning dynamic flag for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " to "
+ + MobileModuleFlags.sanitize(userName));
+
+ JSONObject result = new JSONObject();
+ result.put("flag", dynamicFlag);
+ out.write(result.toString());
+ log.debug("**** END MobileFlagGet ****");
+ }
+
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ }
+
+ private static String errorJson(String message) {
+ JSONObject obj = new JSONObject();
+ obj.put("error", message);
+ return obj.toString();
+ }
+}
diff --git a/src/main/java/servlets/MobileFlagSubmit.java b/src/main/java/servlets/MobileFlagSubmit.java
new file mode 100644
index 000000000..f2764b854
--- /dev/null
+++ b/src/main/java/servlets/MobileFlagSubmit.java
@@ -0,0 +1,172 @@
+package servlets;
+
+import dbProcs.Getter;
+import dbProcs.Setter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.json.JSONObject;
+import utils.Hash;
+import utils.ShepherdLogManager;
+
+/**
+ * Stateless flag-validation endpoint for the Security Shepherd mobile app.
+ *
+ *
Validates the flag the student discovered inside a challenge by recomputing the same
+ * user-specific HMAC that {@link MobileFlagGet} produced. Because validation is HMAC-based rather
+ * than a static hash comparison, each student has a unique flag and flags cannot be shared or
+ * precomputed from the APK.
+ *
+ *
This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see http://www.gnu.org/licenses/.
+ *
+ * @author Sean Duggan
+ */
+public class MobileFlagSubmit extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger log = LogManager.getLogger(MobileFlagSubmit.class);
+
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
+ log.debug("**** servlets.MobileFlagSubmit ****");
+
+ response.setCharacterEncoding("UTF-8");
+ request.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+
+ PrintWriter out = response.getWriter();
+
+ String moduleId = request.getParameter("moduleId");
+ String flag = request.getParameter("flag");
+
+ if (moduleId == null || flag == null) {
+ log.debug("Missing required parameters");
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ out.write(errorJson("Missing required parameters"));
+ return;
+ }
+
+ HttpSession ses = request.getSession(false);
+ if (ses == null
+ || ses.getAttribute("userName") == null
+ || ses.getAttribute("userStamp") == null) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ out.write(errorJson("Not authenticated"));
+ return;
+ }
+
+ String userId = (String) ses.getAttribute("userStamp");
+ String userName = (String) ses.getAttribute("userName");
+
+ String applicationRoot = getServletContext().getRealPath("");
+
+ String baseFlag = MobileModuleFlags.BASE_FLAGS.get(moduleId);
+ if (baseFlag == null) {
+ log.debug("Unknown mobile module ID: " + MobileModuleFlags.sanitize(moduleId));
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ out.write(errorJson("Module not found"));
+ return;
+ }
+
+ // Recompute the same HMAC that MobileFlagGet produced for this user.
+ // Hash.generateUserSolutionKeyOnly uses HmacSHA512 keyed with the server`s
+ // ephemeral key, which is constant for the lifetime of the server process.
+ String expectedFlag = Hash.generateUserSolutionKeyOnly(baseFlag, userName);
+ boolean correct = expectedFlag != null && expectedFlag.equalsIgnoreCase(flag.trim());
+
+ log.debug(
+ "Flag submission for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " by "
+ + MobileModuleFlags.sanitize(userName)
+ + ": "
+ + (correct ? "correct" : "incorrect"));
+
+ if (correct) {
+ // Record completion and award points if this module has a DB entry
+ String dbModuleId = MobileModuleFlags.MODULE_DB_IDS.get(moduleId);
+ if (dbModuleId != null) {
+ try {
+ // Mobile users never go through GetModule, so there may be no results row.
+ // Calling getModuleAddress triggers moduleGetHash which inserts one if missing.
+ Getter.getModuleAddress(applicationRoot, dbModuleId, userId);
+ // checkPlayerResult returns the module name when in-progress (not yet completed).
+ // Returns null when already completed or when there was no row (both safe to skip).
+ String inProgress = Getter.checkPlayerResult(applicationRoot, dbModuleId, userId);
+ if (inProgress != null) {
+ Setter.updatePlayerResult(
+ applicationRoot, dbModuleId, userId, "Mobile App Submission", 1, 1, 1);
+ log.debug(
+ "Score recorded for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " ("
+ + dbModuleId
+ + ") by "
+ + MobileModuleFlags.sanitize(userName));
+ } else {
+ log.debug(
+ "Module "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " already completed by "
+ + MobileModuleFlags.sanitize(userName)
+ + ", no score change");
+ }
+ } catch (Exception e) {
+ // Scoring failure should not fail the flag validation response
+ log.error(
+ "Failed to record score for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + ": "
+ + e.toString());
+ }
+ }
+ }
+
+ JSONObject result = new JSONObject();
+ result.put("correct", correct);
+ out.write(result.toString());
+ log.debug("**** END MobileFlagSubmit ****");
+ }
+
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ }
+
+ private static String errorJson(String message) {
+ JSONObject obj = new JSONObject();
+ obj.put("correct", false);
+ obj.put("message", message);
+ return obj.toString();
+ }
+}
diff --git a/src/main/java/servlets/MobileInsecureApi.java b/src/main/java/servlets/MobileInsecureApi.java
new file mode 100644
index 000000000..f4735ad33
--- /dev/null
+++ b/src/main/java/servlets/MobileInsecureApi.java
@@ -0,0 +1,141 @@
+package servlets;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.json.JSONObject;
+import utils.Hash;
+import utils.ShepherdLogManager;
+
+/**
+ * Intentionally insecure mobile API endpoint used by the Insecure Communication challenges.
+ *
+ *
This endpoint simulates a legacy backend session API that transmits a sensitive session token
+ * over plain HTTP. Students configure a proxy (e.g. Burp Suite, mitmproxy) on their device and
+ * intercept the HTTP response to extract the {@code session_token} field, which is the flag for the
+ * relevant module.
+ *
+ *
Authentication via the Shepherd session cookie (JSESSIONID), which is transmitted in plaintext
+ * over HTTP â€" demonstrating session-hijacking via unencrypted transport.
+ *
+ *
This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ *
+ * @author Sean Duggan
+ */
+public class MobileInsecureApi extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger log = LogManager.getLogger(MobileInsecureApi.class);
+
+ @Override
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
+ log.debug("**** servlets.MobileInsecureApi ****");
+
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+
+ // Intentionally omit security headers to simulate a legacy insecure service
+ response.setHeader("Server", "legacy-api/1.0");
+ response.setHeader("X-Powered-By", "Apache-Coyote/1.1");
+
+ PrintWriter out = response.getWriter();
+
+ String moduleId = request.getParameter("moduleId");
+
+ if (moduleId == null) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ out.write(errorJson("Missing required parameters"));
+ return;
+ }
+
+ HttpSession ses = request.getSession(false);
+ if (ses == null
+ || ses.getAttribute("userName") == null
+ || ses.getAttribute("userStamp") == null) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ out.write(errorJson("Not authenticated"));
+ return;
+ }
+
+ String userName = (String) ses.getAttribute("userName");
+
+ String baseFlag = MobileModuleFlags.BASE_FLAGS.get(moduleId);
+ if (baseFlag == null) {
+ log.debug("Unknown mobile module ID: " + MobileModuleFlags.sanitize(moduleId));
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ out.write(errorJson("Module not found"));
+ return;
+ }
+
+ String sessionToken;
+ try {
+ // Same HMAC derivation as MobileFlagGet / MobileFlagSubmit — unique per user per session
+ sessionToken = Hash.generateUserSolutionKeyOnly(baseFlag, userName);
+ } catch (Exception e) {
+ log.error("Flag generation error: " + e.toString());
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ out.write(errorJson("Server error"));
+ return;
+ }
+
+ // Build a realistic-looking session response with the flag embedded as session_token
+ String userId = "u_" + String.format("%04x", userName.hashCode() & 0xffff);
+ String expiresAt = Instant.now().plus(1, ChronoUnit.HOURS).toString();
+
+ JSONObject json = new JSONObject();
+ json.put("status", "ok");
+ json.put("user_id", userId);
+ json.put("session_token", sessionToken);
+ json.put("role", "user");
+ json.put("server", "legacy-api/1.0");
+ json.put("expires", expiresAt);
+
+ log.debug(
+ "MobileInsecureApi: served session token for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " to "
+ + MobileModuleFlags.sanitize(userName));
+ response.setStatus(HttpServletResponse.SC_OK);
+ out.write(json.toString());
+ }
+
+ private static String errorJson(String message) {
+ return new JSONObject().put("error", message).toString();
+ }
+}
diff --git a/src/main/java/servlets/MobileModuleFlags.java b/src/main/java/servlets/MobileModuleFlags.java
new file mode 100644
index 000000000..1494b2f4c
--- /dev/null
+++ b/src/main/java/servlets/MobileModuleFlags.java
@@ -0,0 +1,111 @@
+package servlets;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Server-side base flag values for mobile modules.
+ *
+ *
These plaintext strings are never sent to the Android client. Only their HMAC
+ * — keyed with the server's ephemeral key and the authenticated user's name — is returned via
+ * {@code MobileFlagGet}. This ensures every student receives a unique flag that cannot be
+ * precomputed by decompiling the APK.
+ *
+ *
Add an entry here when a new mobile module is promoted to server-side validation.
+ *
+ *
This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ *
+ * @author Sean Duggan
+ */
+final class MobileModuleFlags {
+
+ private MobileModuleFlags() {}
+
+ /**
+ * Maps mobile module ID strings to their corresponding database moduleId UUIDs. Used by
+ * MobileFlagSubmit to record completion and award points via Setter.updatePlayerResult.
+ */
+ static final Map MODULE_DB_IDS;
+
+ static final Map BASE_FLAGS;
+
+ static {
+ Map m = new HashMap<>();
+ // Client-Side Injection (M7 / SQLite)
+ m.put("client_side_injection_lesson", "Marble_Rolls_Past_The_Cat");
+ m.put("client_side_injection_challenge_1", "Bitter_Snake_Finds_The_Lock");
+ m.put("client_side_injection_challenge_2", "Hollow_Bridge_Holds_No_Rain");
+ // Poor Authentication (M3) — flag revealed after cracking hardcoded PIN
+ m.put("poor_auth_lesson", "Taco_Snores_On_A_Couch");
+ m.put("poor_auth_challenge", "Cracked_Jar_Holds_Old_Honey");
+ // Insecure Authorization (M3) — flag revealed after privilege escalation via SharedPreferences
+ m.put("insecure_auth_lesson", "Purple_Boots_Dance_On_Ice");
+ // Input Validation (M4) — flag revealed after URL validation bypass
+ m.put("input_validation_lesson", "Seven_Clouds_Chase_A_Kite");
+ // Supply Chain (M6) — flag obtained from vulnerable dependency debug logs
+ m.put("supply_chain_lesson", "Yellow_Lamp_Hums_All_Night");
+ // Reverse Engineering (M9) — offline-only lesson; static flag lives in the APK by design.
+ // Not included here: MobileFlagGet will reject requests for this module, preventing API
+ // farming.
+ // Security Misconfiguration (M1) — flag exposed via exported activity
+ m.put("security_misconfig_lesson", "Quiet_Frogs_Jump_On_Tuesday");
+ m.put("security_misconfig_challenge_2", "Faded_Map_Leads_Nowhere_Fast");
+ // Privacy Controls (M8) — flag embedded in image EXIF metadata
+ m.put("privacy_lesson", "Silver_Moths_Dream_Of_Keys");
+ // Insecure Data Storage (M2) — flag stored as plaintext password in SQLite and
+ // SharedPreferences
+ m.put("ids_lesson", "Copper_Bridge_Stands_In_Rain");
+ m.put("ids_challenge_1", "Tangled_Rope_Skips_A_Stone");
+ // Insecure Communication (M5) — flag transmitted in HTTP JSON response body; intercept with
+ // proxy
+ m.put("insecure_comm_lesson", "Green_Socks_Float_Past_Reeds");
+ m.put("insecure_comm_challenge", "Empty_Wave_Breaks_On_Shore");
+ // Insufficient Cryptography (M6) — flag encrypted with weak DES key
+ m.put("insufficient_crypto_lesson", "Dusty_Mirror_Speaks_In_Whispers");
+ m.put("insufficient_crypto_challenge", "Blind_Fish_Swims_In_Salt");
+ BASE_FLAGS = Collections.unmodifiableMap(m);
+
+ Map ids = new HashMap<>();
+ // Existing mobile modules already in the platform DB
+ ids.put("ids_lesson", "53a53a66cb3bf3e4c665c442425ca90e29536edd");
+ ids.put("ids_challenge_1", "307f78f18fd6a87e50ed6705231a9f24cd582574");
+ // RE challenges use hardcodedKey=1 (client-side SHA-256); DB IDs registered for module list
+ // display
+ ids.put("re_lesson", "2ab09c0c18470ae5f87d219d019a1f603e66f944");
+ ids.put("re_challenge_1", "52885a3db5b09adc24f38bc453fe348f850649b3");
+ ids.put("poor_auth_lesson", "0cdd1549e7c74084d7059ce748b93ef657b44457");
+ ids.put("poor_auth_challenge", "ba6e65e4881c8499b5e53eb33b5be6b5d0f1fb2c");
+ ids.put("client_side_injection_lesson", "335440fef02d19259254ed88293b62f31cccdd41");
+ ids.put("client_side_injection_challenge_1", "a3f7ffd0f9c3d15564428d4df0b91bd927e4e5e4");
+ ids.put("client_side_injection_challenge_2", "e635fce334aa61fdaa459c21c286d6332eddcdd3");
+ // New mobile-specific modules added to the platform
+ ids.put("insecure_auth_lesson", "0f40ae03b9339cb88fbd834213ee1c597791274a");
+ ids.put("insecure_comm_lesson", "a76d11ebd575aecfba5d69441cbd90c95e8abe31");
+ ids.put("insecure_comm_challenge", "d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7");
+ ids.put("insufficient_crypto_lesson", "3385d879b0da97597e16e5c8a7511a6ec331d1d9");
+ ids.put("insufficient_crypto_challenge", "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2");
+ ids.put("security_misconfig_lesson", "c85dad7f468a333e53edaca90a435528db76d118");
+ ids.put("security_misconfig_challenge_2", "e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8");
+ ids.put("input_validation_lesson", "708b76213e50409e138fc68eba81ed7ec8fccf08");
+ ids.put("privacy_lesson", "952c4c3785d8bd8d51bb0d0161c3f6997dd01863");
+ ids.put("supply_chain_lesson", "b9c1f4a7e2d3b5c8f6e1a4d7c2b9f3e8a5d1c6b3");
+ MODULE_DB_IDS = Collections.unmodifiableMap(ids);
+ }
+
+ /**
+ * Strips CR, LF, and tab characters from a user-supplied string before it is interpolated into a
+ * log message, preventing log injection attacks.
+ */
+ static String sanitize(String input) {
+ if (input == null) return "(null)";
+ return input.replaceAll("[\r\n\t]", "_");
+ }
+}
diff --git a/src/main/java/servlets/MobileModuleProgress.java b/src/main/java/servlets/MobileModuleProgress.java
new file mode 100644
index 000000000..5cf5eeafc
--- /dev/null
+++ b/src/main/java/servlets/MobileModuleProgress.java
@@ -0,0 +1,56 @@
+package servlets;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * In-memory tracking of which mobile modules each authenticated user has opened via the app.
+ *
+ *
Used by {@link MobileFlagGet} to gate flag delivery — a flag is only returned once the student
+ * has navigated to the corresponding module through the app. This prevents a student from
+ * bulk-fetching all flags via direct API calls immediately after logging in.
+ *
+ *
State is ephemeral: it resets on server restart. Students who re-open a module after a server
+ * restart will automatically re-trigger {@link MobileModuleStart}, re-recording the start.
+ *
+ *
This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ *
+ * @author Sean Duggan
+ */
+final class MobileModuleProgress {
+
+ private MobileModuleProgress() {}
+
+ /** Entries expire after this duration to prevent unbounded heap growth. */
+ private static final long EXPIRY_MS = 24L * 60 * 60 * 1000;
+
+ private static final ConcurrentHashMap STARTED = new ConcurrentHashMap<>();
+
+ /** Records that {@code userId} has opened {@code moduleId} in the app. */
+ static void recordStart(String userId, String moduleId) {
+ STARTED.put(userId + "|" + moduleId, System.currentTimeMillis());
+ // Probabilistic cleanup: purge expired entries on ~1% of calls to bound heap usage.
+ if (ThreadLocalRandom.current().nextDouble() < 0.01) {
+ long cutoff = System.currentTimeMillis() - EXPIRY_MS;
+ STARTED.values().removeIf(ts -> ts < cutoff);
+ }
+ }
+
+ /** Returns {@code true} if {@code userId} has previously opened {@code moduleId}. */
+ static boolean hasStarted(String userId, String moduleId) {
+ Long ts = STARTED.get(userId + "|" + moduleId);
+ if (ts == null) return false;
+ if (System.currentTimeMillis() - ts > EXPIRY_MS) {
+ STARTED.remove(userId + "|" + moduleId);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/servlets/MobileModuleStart.java b/src/main/java/servlets/MobileModuleStart.java
new file mode 100644
index 000000000..661843aaa
--- /dev/null
+++ b/src/main/java/servlets/MobileModuleStart.java
@@ -0,0 +1,150 @@
+package servlets;
+
+import dbProcs.Getter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.json.JSONObject;
+import utils.ShepherdLogManager;
+
+/**
+ * Records that an authenticated student has opened a mobile module in the app.
+ *
+ *
{@link MobileFlagGet} will not return a flag for a module until this endpoint has been called
+ * for that user/module pair. The Android app calls this automatically when a lesson or challenge
+ * fragment becomes visible, so legitimate use is transparent to the student.
+ *
+ *
This two-step requirement (start → get) prevents a student from bulk-fetching all flags via
+ * direct API calls immediately after logging in without interacting with any module.
+ *
+ *
For offline-only modules (e.g. Reverse Engineering) that have no entry in {@link
+ * MobileModuleFlags#BASE_FLAGS}, this endpoint silently succeeds — the gate in {@code
+ * MobileFlagGet} is never reached for those modules anyway.
+ *
+ *
This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ *
+ * @author Sean Duggan
+ */
+public class MobileModuleStart extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger log = LogManager.getLogger(MobileModuleStart.class);
+
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
+ log.debug("**** servlets.MobileModuleStart ****");
+
+ response.setCharacterEncoding("UTF-8");
+ request.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+
+ PrintWriter out = response.getWriter();
+
+ String moduleId = request.getParameter("moduleId");
+
+ if (moduleId == null) {
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ out.write(errorJson("Missing required parameters"));
+ return;
+ }
+
+ HttpSession ses = request.getSession(false);
+ if (ses == null
+ || ses.getAttribute("userName") == null
+ || ses.getAttribute("userStamp") == null) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ out.write(errorJson("Not authenticated"));
+ return;
+ }
+
+ String userId = (String) ses.getAttribute("userStamp");
+
+ String applicationRoot = getServletContext().getRealPath("");
+
+ // Offline-only modules (not in BASE_FLAGS) have no server-side flag to gate.
+ // Return success immediately so the app does not need special-case handling.
+ if (!MobileModuleFlags.BASE_FLAGS.containsKey(moduleId)) {
+ JSONObject result = new JSONObject();
+ result.put("started", true);
+ out.write(result.toString());
+ log.debug(
+ "Module start: "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " is offline-only, silently accepted");
+ return;
+ }
+
+ // Record in-memory for fast gate checks within this server session.
+ MobileModuleProgress.recordStart(userId, moduleId);
+
+ // Also DB-persist via moduleGetHash so the start survives server restarts.
+ // getModuleAddress is idempotent: it only inserts a results row if one doesn't exist.
+ String dbModuleId = MobileModuleFlags.MODULE_DB_IDS.get(moduleId);
+ if (dbModuleId != null) {
+ try {
+ Getter.getModuleAddress(applicationRoot, dbModuleId, userId);
+ log.debug(
+ "DB start row ensured for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + " ("
+ + dbModuleId
+ + ")");
+ } catch (Exception e) {
+ // Non-fatal: in-memory record is still set; gate will fall back to DB on next check.
+ log.warn(
+ "DB start record failed for "
+ + MobileModuleFlags.sanitize(moduleId)
+ + ": "
+ + e.toString());
+ }
+ }
+
+ log.debug(
+ "Module start recorded: " + MobileModuleFlags.sanitize(moduleId) + " for userId " + userId);
+
+ JSONObject result = new JSONObject();
+ result.put("started", true);
+ out.write(result.toString());
+ log.debug("**** END MobileModuleStart ****");
+ }
+
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ }
+
+ private static String errorJson(String message) {
+ JSONObject obj = new JSONObject();
+ obj.put("error", message);
+ return obj.toString();
+ }
+}
diff --git a/src/main/java/servlets/MobileRegister.java b/src/main/java/servlets/MobileRegister.java
new file mode 100644
index 000000000..5b43255db
--- /dev/null
+++ b/src/main/java/servlets/MobileRegister.java
@@ -0,0 +1,137 @@
+package servlets;
+
+import dbProcs.Getter;
+import dbProcs.Setter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.sql.SQLException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.json.JSONObject;
+import utils.OpenRegistration;
+import utils.ShepherdLogManager;
+import utils.Validate;
+
+/**
+ * Stateless registration endpoint for the Security Shepherd mobile app.
+ *
+ * Unlike the web {@link Register} servlet, this endpoint does not require a session or CSRF token
+ * because registration is always initiated by a human using the mobile app's sign-in screen. All
+ * standard input validation (username format, password strength, email format) is still enforced.
+ *
+ *
This file is part of the Security Shepherd Project.
+ *
+ *
The Security Shepherd project is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ *
You should have received a copy of the GNU General Public License along with the Security
+ * Shepherd project. If not, see .
+ *
+ * @author Sean Duggan
+ */
+public class MobileRegister extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger log = LogManager.getLogger(MobileRegister.class);
+
+ public void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ ShepherdLogManager.setRequestIp(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"));
+ log.debug("**** servlets.MobileRegister ****");
+
+ response.setCharacterEncoding("UTF-8");
+ request.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+
+ PrintWriter out = response.getWriter();
+
+ if (!OpenRegistration.isEnabled()) {
+ log.debug("Registration is closed");
+ out.write(errorJson("Registration is currently closed on this server"));
+ return;
+ }
+
+ String userName = request.getParameter("userName");
+ String passWord = request.getParameter("passWord");
+ String userAddress = request.getParameter("userAddress");
+
+ if (userName == null || userName.isEmpty() || passWord == null || passWord.isEmpty()) {
+ out.write(errorJson("Username and password are required"));
+ return;
+ }
+
+ if (userAddress == null) {
+ userAddress = "";
+ }
+
+ boolean validAddress = userAddress.isEmpty() || Validate.isValidEmailAddress(userAddress);
+
+ boolean userValidate;
+ if (validAddress && !userAddress.isEmpty()) {
+ userValidate = Validate.isValidUser(userName, passWord, userAddress);
+ } else {
+ userValidate = Validate.isValidUser(userName, passWord);
+ }
+
+ if (!userValidate) {
+ out.write(errorJson("Invalid username or password format"));
+ return;
+ }
+
+ if (!validAddress) {
+ out.write(errorJson("Invalid email address format"));
+ return;
+ }
+
+ String applicationRoot = getServletContext().getRealPath("");
+ try {
+ String defaultClass = Getter.getDefaultClass(applicationRoot);
+ String classId = (defaultClass == null || defaultClass.isEmpty()) ? null : defaultClass;
+ Setter.userCreate(applicationRoot, classId, userName, passWord, "player", userAddress, false);
+ log.debug("Mobile registration successful for: " + userName);
+ JSONObject result = new JSONObject();
+ result.put("success", true);
+ out.write(result.toString());
+ } catch (SQLException e) {
+ log.error("Registration DB error: " + e.toString());
+ out.write(errorJson("Registration failed — username may already be taken"));
+ } catch (Exception e) {
+ log.error("Registration error: " + e.toString());
+ out.write(errorJson("Server error"));
+ }
+
+ log.debug("**** END MobileRegister ****");
+ }
+
+ public void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ }
+
+ private static String errorJson(String message) {
+ JSONObject obj = new JSONObject();
+ obj.put("success", false);
+ obj.put("message", message);
+ return obj.toString();
+ }
+}
diff --git a/src/main/resources/database/coreSchema.sql b/src/main/resources/database/coreSchema.sql
index 12b74f0c1..682609b1f 100644
--- a/src/main/resources/database/coreSchema.sql
+++ b/src/main/resources/database/coreSchema.sql
@@ -1580,57 +1580,42 @@ INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleT
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('2ab09c0c18470ae5f87d219d019a1f603e66f944', 'Reverse Engineering', 'reverse.engineering', 'lesson', 'Mobile Reverse Engineering', 'mobile.reverse.engineering', 'DrumaDrumaDrumBoomBoom', '19753b944b63232812b7af1a0e0adb59928158da5994a39f584cb799f25a95b9', 'open', '75', '40', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('a4bf43f2ba5ced041b03d0b3f9fa3541c520d65d', 'Session Management Challenge 1', 'session.management.challenge.1', 'challenge', 'Session Management', 'session.management', 'db7b1da5d7a43c7100a6f01bb0c', 'dfd6bfba1033fa380e378299b6a998c759646bd8aea02511482b8ce5d707f93a', 'open', '75', '40', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('3d5b46abc6865ba09aaff98a8278a5f5e339abff', 'Failure to Restrict URL Access 1', 'failure.to.restrict.url.access.1', 'challenge', 'Failure to Restrict URL Access', 'failure.to.restrict.url.access', 'c776572b6a9d5b5c6e4aa672a4771213', '4a1bc73dd68f64107db3bbc7ee74e3f1336d350c4e1e51d4eda5b52dddf86c99', 'open', '76', '40', 0, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('1506f22cd73d14d8a73e0ee32006f35d4f234799', 'Unintended Data Leakage', 'unintended.data.leakage', 'lesson', 'Mobile Data Leakage', 'mobile.data.leakage', 'SilentButSteadyRedLed', '392c20397c535845d93c32fd99b94f70afe9cca3f78c1e4766fee1cc08c035ec', 'open', '77', '40', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('453d22238401e0bf6f1ff5d45996407e98e45b07', 'Cross Site Request Forgery', 'cross.site.request.forgery', 'lesson', 'CSRF', 'csrf', '666980771c29857b8a84c686751ce7edaae3d6ac1', 'ed4182af119d97728b2afca6da7cdbe270a9e9dd714065f0f775cd40dc296bc7', 'open', '78', '40', 0, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('5b461ebe2e5e2797740cb3e9c7e3f93449a93e3a', 'Content Provider Leakage', 'content.provider.leakage', 'lesson', 'Mobile Content Provider', 'mobile.content.provider', 'LazerLizardsFlamingWizards', '4d41997b5b81c88f7eb761c1975481c4ce397b80291d99307cfad69662277d39', 'open', '79', '50', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('6f5db377c28da4179bca1a43ede8d6bcf7bd322e', 'Untrusted Input', 'untrusted.input', 'lesson', 'Mobile Security Decisions via Untrusted Input', 'mobile.security.decisions.via.untrusted.input', 'RetroMagicFuturePunch', '5e2b61c679d1f290d23308b3b66c3ec00cd069f1483b705d17f2795a4e77dcb6', 'open', '82', '50', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('52885a3db5b09adc24f38bc453fe348f850649b3', 'Reverse Engineering 1', 'reverse.engineering.1', 'challenge', 'Mobile Reverse Engineering', 'mobile.reverse.engineering', 'christopherjenkins', '072a9e4fc888562563adf8a89fa55050e3e1cfbbbe1d597b0537513ac8665295', 'open', '85', '50', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('b6432a6b5022cb044e9946315c44ab262ab59e88', 'Unvalidated Redirects and Forwards', 'unvalidated.redirects.and.forwards', 'lesson', 'Unvalidated Redirects and Forwards', 'unvalidated.redirects.and.forwards', '658c43abcf81a61ca5234cfd7a2', 'f15f2766c971e16e68aa26043e6016a0a7f6879283c873d9476a8e7e94ea736f', 'open', '86', '45', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('335440fef02d19259254ed88293b62f31cccdd41', 'Client Side Injection', 'client.side.injection', 'lesson', 'Mobile Injection', 'mobile.injection', 'VolcanicEruptionsAbruptInterruptions', 'f758a97011ec4452cc0707e546a7c0f68abc6ef2ab747ea87e0892767152eae1', 'open', '87', '50', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('f771a10efb42a79a9dba262fd2be2e44bf40b66d', 'SQL Injection 2', 'sql.injection.2', 'challenge', 'Injection', 'injection', 'f62abebf5658a6a44c5c9babc7865110c62f5ecd9d0a7052db48c4dbee0200e3', 'ffd39cb26727f34cbf9fce3e82b9d703404e99cdef54d2aa745f497abe070b', 'open', '88', '45', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('d7eaeaa1cc4f218abd86d14eefa183a0f8eb6298', 'NoSQL Injection One', 'nosql.injection.one', 'challenge', 'Injection', 'injection', 'c09f32d4c3dd5b75f04108e5ffc9226cd8840288a62bdaf9dc65828ab6eaf86a', 'd63c2fb5da9b81ca26237f1308afe54491d1bacf9fffa0b21a072b03c5bafe66', 'open', '89', '45', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('0cdd1549e7c74084d7059ce748b93ef657b44457', 'Poor Authentication', 'poor.authentication', 'lesson', 'Mobile Poor Authentication', 'mobile.poor.authentication', 'UpsideDownPizzaDip', '77777b312d5b56a17c1f30550dd34e8d6bd8b037f05341e64e94f5411c10ac8e', 'open', '90', '50', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('ef6496892b8e48ac2f349cdd7c8ecb889fc982af', 'Broken Crypto', 'broken.crypto', 'lesson', 'Mobile Broken Crypto', 'mobile.broken.crypto', '33edeb397d665ed7d1a580f3148d4b2f', '911fa7f4232e096d6a74a0623842c4157e29b9bcc44e8a827be3bb7e58c9a212', 'open', '97', '50', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('f16bf2ab1c1bf400d36330f91e9ac6045edcd003', 'Reverse Engineering 2', 'reverse.engineering.2', 'challenge', 'Mobile Reverse Engineering', 'mobile.reverse.engineering', 'FireStoneElectric', '5bc811f9e744a71393a277c51bfd8fbb5469a60209b44fa3485c18794df4d5b1', 'open', '98', '50', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('c685f8102ae0128c2ab342df64699bb8209a0839', 'SQL Injection Escaping', 'sql.injection.escaping', 'challenge', 'Injection', 'injection', '0dcf9078ba5d878f9e23809ac8f013d1a08fdc8f12c5036f1a4746dbe86c0aac', '8c3c35c30cdbbb73b7be3a4f8587aa9d88044dc43e248984a252c6e861f673d4', 'open', '99', '50', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('b70a84f159876bb9885b6e0087d22f0a52abbfcf', 'Session Management Challenge 2', 'session.management.challenge.2', 'challenge', 'Session Management', 'session.management', '4ba31e5ffe29de092fe1950422a', 'd779e34a54172cbc245300d3bc22937090ebd3769466a501a5e7ac605b9f34b7', 'open', '105', '55', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('20e755179a5840be5503d42bb3711716235005ea', 'CSRF 1', 'csrf.1', 'challenge', 'CSRF', 'csrf', '7639c952a191d569a0c741843b599604c37e33f9f5d8eb07abf0254635320b07', 's74a796e84e25b854906d88f622170c1c06817e72b526b3d1e9a6085f429cf52', 'open', '106', '55', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('0e9e650ffca2d1fe516c5d7b0ce5c32de9e53d1e', 'Session Management Challenge 3', 'session.management.challenge.3', 'challenge', 'Session Management', 'session.management', 'e62008dc47f5eb065229d48963', 't193c6634f049bcf65cdcac72269eeac25dbb2a6887bdb38873e57d0ef447bc3', 'open', '115', '60', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('307f78f18fd6a87e50ed6705231a9f24cd582574', 'Insecure Data Storage 1', 'insecure.data.storage.1', 'challenge', 'Mobile Insecure Data Storage', 'mobile.insecure.data.storage', 'WarshipsAndWrenches', '362f84cf26bf96aeae358d5d0bbee31e9291aaa5367594c29b3af542a7572c01', 'open', '116', '60', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('3f010a976bcbd6a37fba4a10e4a057acc80bdc09', 'Broken Crypto 1', 'broken.crypto.1', 'challenge', 'Mobile Broken Crypto', 'mobile.broken.crypto', 'd1f2df53084b970ab538457f5af34c8b', 'd2f8519f8264f9479f56165465590b499ceca941ab848805c00f5bf0a40c9717', 'open', '117', '60', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('d4e2c37d8f1298fcaf4edcea7292cb76e9eab09b', 'Cross Site Scripting 2', 'cross.site.scripting.2', 'challenge', 'XSS', 'xss', '495ab8cc7fe9532c6a75d378de', 't227357536888e807ff0f0eff751d6034bafe48954575c3a6563cb47a85b1e888', 'open', '119', '60', 0, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('9e46e3c8bde42dc16b9131c0547eedbf265e8f16', 'Reverse Engineering 3', 'reverse.engineering.3', 'challenge', 'Mobile Reverse Engineering', 'mobile.reverse.engineering', 'C1babd72225f0e9934YZ8', 'dbae0baa3f71f196c4d2c6c984d45a6c1c635bf1b482dccfe32e9b01b69a042b', 'open', '120', '76', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('0709410108f91314fb6f7721df9b891351eb2fcc', 'Insecure Cryptographic Storage Challenge 2', 'insecure.cryptographic.storage.challenge.2', 'challenge', 'Insecure Cryptographic Storage', 'insecure.cryptographic.storage', 'TheVigenereCipherIsAmethodOfEncryptingAlphabeticTextByUsingPoly', 'h8aa0fdc145fb8089661997214cc0e685e5f86a87f30c2ca641e1dde15b01177', 'open', '126', '65', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('82e8e9e2941a06852b90c97087309b067aeb2c4c', 'Insecure Direct Object Reference Challenge 2', 'insecure.direct.object.reference.challenge.2', 'challenge', 'Insecure Direct Object References', 'insecure.direct.object.references', '1f746b87a4e3628b90b1927de23f6077abdbbb64586d3ac9485625da21921a0f', 'vc9b78627df2c032ceaf7375df1d847e47ed7abac2a4ce4cb6086646e0f313a4', 'open', '127', '65', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('6319a2e38cc4b2dc9e6d840e1b81db11ee8e5342', 'Cross Site Scripting 3', 'cross.site.scripting.3', 'challenge', 'XSS', 'xss', '6abaf491c9122db375533c04df', 'ad2628bcc79bf10dd54ee62de148ab44b7bd028009a908ce3f1b4d019886d0e', 'open', '128', '65', 0, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('da3de2e556494a9c2fb7308a98454cf55f3a4911', 'Insecure Data Storage 2', 'insecure.data.storage.2', 'challenge', 'Mobile Insecure Data Storage', 'mobile.insecure.data.storage', 'starfish123', 'ec09515a304d2de1f552e961ab769967bdc75740ad2363803168b7907c794cd4', 'open', '129', '65', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('cb7d696bdf88899e8077063d911fc8da14176702', 'Insecure Data Storage 3', 'insecure.data.storage.3', 'challenge', 'Mobile Insecure Data Storage', 'mobile.insecure.data.storage', 'c4ptainBrunch', '11ccaf2f3b2aa4f88265b9cacb5e0ed26b11af978523e34528cf0bb9d32de851', 'open', '130', '60', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('de626470273c01388629e5a56ac6f17e2eef957b', 'Insecure Direct Object Reference Bank', 'insecure.direct.object.reference.bank', 'challenge', 'Insecure Direct Object References', 'insecure.direct.object.references', '4a1df02af317270f844b56edc0c29a09f3dd39faad3e2a23393606769b2dfa35', '1f0935baec6ba69d79cfb2eba5fdfa6ac5d77fadee08585eb98b130ec524d00c', 'open', '131', '60', 0, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('f40b0cd5d45327c9426675313f581cf70c7c7c28', 'Unintended Data Leakage 1', 'unintended.data.leakage.1', 'challenge', 'Mobile Data Leakage', 'mobile.data.leakage', 'BagsofSalsa', '517622a535ff89f7d90674862740b48f53aad7b41390fe46c6f324fee748d136', 'open', '132', '60', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('5dda8dc216bd6a46fccaa4ed45d49404cdc1c82e', 'SQL Injection 3', 'sql.injection.3', 'challenge', 'Injection', 'injection', '9815 1547 3214 7569', 'b7327828a90da59df54b27499c0dc2e875344035e38608fcfb7c1ab8924923f6', 'open', '135', '70', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('94cd2de560d89ef59fc450ecc647ff4d4a55c15d', 'CSRF 2', 'csrf.2', 'challenge', 'CSRF', 'csrf', '45309dbaf8eaf6d1a5f1ecb1bf1b6be368a6542d3da35b9bf0224b88408dc001', 'z311736498a13604705d608fb3171ebf49bc18753b0ec34b8dff5e4f9147eb5e', 'open', '136', '70', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('5ca9115f3279b9b9f3308eb6a59a4fcd374846d6', 'CSRF 3', 'csrf.3', 'challenge', 'CSRF', 'csrf', '6bdbe1901cbe2e2749f347efb9ec2be820cc9396db236970e384604d2d55b62a', 'z6b2f5ebbe112dd09a6c430a167415820adc5633256a7b44a7d1e262db105e3c', 'open', '137', '70', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('a3f7ffd0f9c3d15564428d4df0b91bd927e4e5e4', 'Client Side Injection 1', 'client.side.injection.1', 'challenge', 'Mobile Injection', 'mobile.injection', 'SourHatsAndAngryCats', '8855c8bb9df4446a546414562eda550520e29f7a82400a317c579eb3a5a0a8ef', 'open', '138', '70', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('cfbf7b915ee56508ad46ab79878f37fd9afe0d27', 'CSRF 4', 'csrf.4', 'challenge', 'CSRF', 'csrf', 'bb78f73c7efefec25e518c3a91d50d789b689c4515b453b6140a2e4e1823d203', '84118752e6cd78fecc3563ba2873d944aacb7b72f28693a23f9949ac310648b5', 'open', '139', '70', 0, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('1e3c02ad49fa9a9e396a3b268d7da8f0b647d8f9', 'Unintended Data Leakage 2', 'unintended.data.leakage.2', 'challenge', 'Mobile Data Leakage', 'mobile.data.leakage', '627884736748', '85ceae7ec397c8f4448be51c33a634194bf5da440282227c15954bbdfb54f0c7', 'open', '140', '70', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('fcc1918e0a23b8420e173cf8029876cb887408d3', 'CSRF JSON', 'csrf.json', 'challenge', 'CSRF', 'csrf', 'f57f1377bd847a370d42e1410bfe48c9a3484e78d50e83f851b634fe77d41a6e', '2e0981dcb8278a57dcfaae3b8da0c78d5a70c2d38ea9d8b3e14db3aea01afcbb', 'open', '141', '70', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('ced925f8357a17cfe3225c6236df0f681b2447c4', 'Session Management Challenge 4', 'session.management.challenge.4', 'challenge', 'Session Management', 'session.management', '238a43b12dde07f39d14599a780ae90f87a23e', 'ec43ae137b8bf7abb9c85a87cf95c23f7fadcf08a092e05620c9968bd60fcba6', 'open', '145', '75', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('182f519ef2add981c77a584380f41875edc65a56', 'Cross Site Scripting 4', 'cross.site.scripting.4', 'challenge', 'XSS', 'xss', '515e05137e023dd7828adc03f639c8b13752fbdffab2353ccec', '06f81ca93f26236112f8e31f32939bd496ffe8c9f7b564bce32bd5e3a8c2f751', 'open', '146', '75', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('e0ba96bb4c8d4cd2e1ff0a10a0c82b5362edf998', 'SQL Injection 4', 'sql.injection.4', 'challenge', 'Injection', 'injection', 'd316e80045d50bdf8ed49d48f130b4acf4a878c82faef34daff8eb1b98763b6f', '1feccf2205b4c5ddf743630b46aece3784d61adc56498f7603ccd7cb8ae92629', 'open', '147', '75', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('b3cfd5890649e6815a1c7107cc41d17c82826cfa', 'Insecure Cryptographic Storage Challenge 3', 'insecure.cryptographic.storage.challenge.3', 'challenge', 'Insecure Cryptographic Storage', 'insecure.cryptographic.storage', 'THISISTHESECURITYSHEPHERDABCENCRYPTIONKEY', '2da053b4afb1530a500120a49a14d422ea56705a7e3fc405a77bc269948ccae1', 'open', '148', '75', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('63bc4811a2e72a7c833962e5d47a41251cd90de3', 'Broken Crypto 2', 'broken.crypto.2', 'challenge', 'Mobile Broken Crypto', 'mobile.broken.crypto', 'DancingRobotChilliSauce', 'fb5c9ce0f5539b737e534fd317befff7427f6610ed626dfd43abf35295f106bc', 'open', '149', '75', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('e635fce334aa61fdaa459c21c286d6332eddcdd3', 'Client Side Injection 2', 'client.side.injection.2', 'challenge', 'Mobile Injection', 'mobile.injection', 'BurpingChimneys', 'cfe68711def42bb0b201467b859322dd2750f633246842280dc68c858d208425', 'open', '155', '80', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('0a37cb9296ff3763f7f3a45ff313bce47afa9384', 'CSRF 5', 'csrf.5', 'challenge', 'CSRF', 'csrf', '8f34078ef3e53f619618d9def1ede8a6a9117c77c2fad22f76bba633da83e6d4', '70b96195472adf3bf347cbc37c34489287969d5ba504ac2439915184d6e5dc49', 'open', '156', '80', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('3b14ca3c8f9b90c9b2c8cd1fba9fa67add1272a3', 'Poor Data Validation 2', 'poor.data.validation.2', 'challenge', 'Poor Data Validation', 'poor.data.validation', '05adf1e4afeb5550faf7edbec99170b40e79168ecb3a5da19943f05a3fe08c8e', '20e8c4bb50180fed9c1c8d1bf6af5eac154e97d3ce97e43257c76e73e3bbe5d5', 'open', '157', '80', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('ba6e65e4881c8499b5e53eb33b5be6b5d0f1fb2c', 'Poor Authentication 1', 'poor.authentication.1', 'challenge', 'Mobile Poor Authentication', 'mobile.poor.authentication', 'MegaKillerExtremeCheese', 'efa08298fc6a4add4b9a4bbdbbbb18ac934667971fa275bd7d234589bd8a8467', 'open', '160', '60', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('c7ac1e05faa2d4b1016cfcc726e0689419662784', 'Failure to Restrict URL Access 2', 'failure.to.restrict.url.access.2', 'challenge', 'Failure to Restrict URL Access', 'failure.to.restrict.url.access', '40b675e3d404c52b36abe31d05842b283975ec62e8', '278fa30ee727b74b9a2522a5ca3bf993087de5a0ac72adff216002abf79146fa', 'open', '165', '85', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('fccf8e4d5372ee5a73af5f862dc810545d19b176', 'Cross Site Scripting 5', 'cross.site.scripting.5', 'challenge', 'XSS', 'xss', '7d7cc278c30cca985ab027e9f9e09e2f759e5a3d1f63293', 'f37d45f597832cdc6e91358dca3f53039d4489c94df2ee280d6203b389dd5671', 'open', '166', '85', 0, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('064e28ea4b2f7708b8cb4495d9db1a5e05decdb8', 'Poor Authentication 2', 'poor.authentication.2','challenge', 'Mobile Poor Authentication', 'mobile.poor.authentication', 'MoreRobotsNotEnoughNuts', '808d8372ec7bc7e37e8e3b30d313cb47763926065a4623b27b24cc537fee72a7', 'open', '173', '70', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('a84bbf8737a9ca749d81d5226fc87e0c828138ee', 'SQL Injection 5', 'sql.injection.5', 'challenge', 'Injection', 'injection', '343f2e424d5d7a2eff7f9ee5a5a72fd97d5a19ef7bff3ef2953e033ea32dd7ee', '8edf0a8ed891e6fef1b650935a6c46b03379a0eebab36afcd1d9076f65d4ce62', 'open', '175', '90', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('04a5bd8656fdeceac26e21ef6b04b90eaafbd7d5', 'CSRF 6', 'csrf.6', 'challenge', 'CSRF', 'csrf', 'df611f54325786d42e6deae8bbd0b9d21cf2c9282ec6de4e04166abe2792ac00', '2fff41105149e507c75b5a54e558470469d7024929cf78d570cd16c03bee3569', 'open', '176', '90', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('dc89383763c68cba0aaa1c6f3fd4c17e9d49a805', 'SQL Injection Stored Procedure', 'sql.injection.stored.procedure', 'challenge', 'Injection', 'injection', 'd9c5757c1c086d02d491cbe46a941ecde5a65d523de36ac1bfed8dd4dd9994c8', '7edcbc1418f11347167dabb69fcb54137960405da2f7a90a0684f86c4d45a2e7', 'open', '177', '90', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('6a411618a05e3cef8ccb6f3d7914412d27782a88', 'Content Provider Leakage 1', 'content.provider.leakage.1', 'challenge', 'Mobile Content Provider', 'mobile.content.provider', 'BlueCupNoPartySorry', '2a845ec1943a6342956a48cdc8ca60f40036b68a810109d0b9d2a35271377980', 'open', '178', '75', 1, 0);
-INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('3b1af0ad239325bf494c6e606585320b31612e72', 'Broken Crypto 3', 'broken.crypto.3', 'challenge', 'Mobile Broken Crypto', 'mobile.broken.crypto', 'ShaveTheSkies', 'f5a3f19dd44b53c6d29dda65fa90791bb312a3044b3110acb8a65d165376bf34', 'open', '180', '180', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('c6841bcc326c4bad3a23cd4fa6391eb9bdb146ed', 'Cross Site Scripting 6', 'cross.site.scripting.6', 'challenge', 'XSS', 'xss', 'c13e42171dbd41a7020852ffdd3399b63a87f5', 'd330dea1acf21886b685184ee222ea8e0a60589c3940afd6ebf433469e997caf', 'open', '185', '95', 0, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('ad332a32a6af1f005f9c8d1e98db264eb2ae5dfe', 'SQL Injection 6', 'sql.injection.6', 'challenge', 'Injection', 'injection', '17f999a8b3fbfde54124d6e94b256a264652e5087b14622e1644c884f8a33f82', 'd0e12e91dafdba4825b261ad5221aae15d28c36c7981222eb59f7fc8d8f212a2', 'open', '186', '95', 1, 0);
INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('ed732e695b85baca21d80966306a9ab5ec37477f', 'Session Management Challenge 5', 'session.management.challenge.5', 'challenge', 'Session Management', 'session.management', 'a15b8ea0b8a3374a1dedc326dfbe3dbae26', '7aed58f3a00087d56c844ed9474c671f8999680556c127a19ee79fa5d7a132e1', 'open', '205', '110', 0, 0);
@@ -1696,17 +1681,9 @@ CALL cheatSheetCreate('ced925f8357a17cfe3225c6236df0f681b2447c4', 'ced925f8357a1
CALL cheatSheetCreate('c6841bcc326c4bad3a23cd4fa6391eb9bdb146ed', 'c6841bcc326c4bad3a23cd4fa6391eb9bdb146ed.solution');
CALL cheatsheetCreate('53a53a66cb3bf3e4c665c442425ca90e29536edd', '53a53a66cb3bf3e4c665c442425ca90e29536edd.solution');
CALL cheatsheetCreate('307f78f18fd6a87e50ed6705231a9f24cd582574', '307f78f18fd6a87e50ed6705231a9f24cd582574.solution');
-CALL cheatsheetCreate('da3de2e556494a9c2fb7308a98454cf55f3a4911', 'da3de2e556494a9c2fb7308a98454cf55f3a4911.solution');
CALL cheatsheetCreate('335440fef02d19259254ed88293b62f31cccdd41', '335440fef02d19259254ed88293b62f31cccdd41.solution');
CALL cheatsheetCreate('a3f7ffd0f9c3d15564428d4df0b91bd927e4e5e4', 'a3f7ffd0f9c3d15564428d4df0b91bd927e4e5e4.solution');
CALL cheatsheetCreate('e635fce334aa61fdaa459c21c286d6332eddcdd3', 'e635fce334aa61fdaa459c21c286d6332eddcdd3.solution');
-CALL cheatsheetCreate('ef6496892b8e48ac2f349cdd7c8ecb889fc982af', 'ef6496892b8e48ac2f349cdd7c8ecb889fc982af.solution');
-CALL cheatsheetCreate('3f010a976bcbd6a37fba4a10e4a057acc80bdc09', '3f010a976bcbd6a37fba4a10e4a057acc80bdc09.solution');
-CALL cheatsheetCreate('63bc4811a2e72a7c833962e5d47a41251cd90de3', '63bc4811a2e72a7c833962e5d47a41251cd90de3.solution');
-CALL cheatsheetCreate('2ab09c0c18470ae5f87d219d019a1f603e66f944', '2ab09c0c18470ae5f87d219d019a1f603e66f944.solution');
-CALL cheatsheetCreate('f16bf2ab1c1bf400d36330f91e9ac6045edcd003', 'f16bf2ab1c1bf400d36330f91e9ac6045edcd003.solution');
-CALL cheatsheetCreate('9e46e3c8bde42dc16b9131c0547eedbf265e8f16', '9e46e3c8bde42dc16b9131c0547eedbf265e8f16.solution');
-CALL cheatsheetCreate('1506f22cd73d14d8a73e0ee32006f35d4f234799', '1506f22cd73d14d8a73e0ee32006f35d4f234799.solution');
CALL cheatSheetCreate('ed732e695b85baca21d80966306a9ab5ec37477f', 'ed732e695b85baca21d80966306a9ab5ec37477f.solution');
CALL cheatSheetCreate('cfbf7b915ee56508ad46ab79878f37fd9afe0d27', 'cfbf7b915ee56508ad46ab79878f37fd9afe0d27.solution');
CALL cheatSheetCreate('9294ba32bdbd680e3260a0315cd98bf6ce8b69bd', '9294ba32bdbd680e3260a0315cd98bf6ce8b69bd.solution');
@@ -1714,11 +1691,9 @@ CALL cheatSheetCreate('7153290d128cfdef5f40742dbaeb129a36ac2340', '7153290d128cf
CALL cheatSheetCreate('145111e80400e4fd48bd3aa5aca382e9c5640793', '145111e80400e4fd48bd3aa5aca382e9c5640793.solution');
CALL cheatSheetCreate('adc845f9624716eefabcc90d172bab4096fa2ac4', 'adc845f9624716eefabcc90d172bab4096fa2ac4.solution');
CALL cheatSheetCreate('64070f5aec0593962a29a141110b9239d73cd7b3', '64070f5aec0593962a29a141110b9239d73cd7b3.solution');
-CALL cheatSheetCreate('1e3c02ad49fa9a9e396a3b268d7da8f0b647d8f9', '1e3c02ad49fa9a9e396a3b268d7da8f0b647d8f9.solution');
-CALL cheatSheetCreate('f40b0cd5d45327c9426675313f581cf70c7c7c28', 'f40b0cd5d45327c9426675313f581cf70c7c7c28.solution');
CALL cheatSheetCreate('ba6e65e4881c8499b5e53eb33b5be6b5d0f1fb2c', 'ba6e65e4881c8499b5e53eb33b5be6b5d0f1fb2c.solution');
CALL cheatSheetCreate('52885a3db5b09adc24f38bc453fe348f850649b3', '52885a3db5b09adc24f38bc453fe348f850649b3.solution');
-CALL cheatSheetCreate('3b1af0ad239325bf494c6e606585320b31612e72', '3b1af0ad239325bf494c6e606585320b31612e72.solution');
+CALL cheatsheetCreate('2ab09c0c18470ae5f87d219d019a1f603e66f944', '2ab09c0c18470ae5f87d219d019a1f603e66f944.solution');
CALL cheatSheetCreate('0cdd1549e7c74084d7059ce748b93ef657b44457', '0cdd1549e7c74084d7059ce748b93ef657b44457.solution');
CALL cheatSheetCreate('368491877a0318e9a774ba5d648c33cb0165ba1e', '368491877a0318e9a774ba5d648c33cb0165ba1e.solution');
CALL cheatSheetCreate('6be5de81223cc1b38b6e427cc44f8b6a28d2bc96', '6be5de81223cc1b38b6e427cc44f8b6a28d2bc96.solution');
@@ -1729,15 +1704,33 @@ CALL cheatSheetCreate('fcc1918e0a23b8420e173cf8029876cb887408d3', 'fcc1918e0a23b
CALL cheatSheetCreate('6158a695f20f9286d5f12ff3f4d42678f4a9740c', '6158a695f20f9286d5f12ff3f4d42678f4a9740c.solution');
CALL cheatSheetCreate('de626470273c01388629e5a56ac6f17e2eef957b', 'de626470273c01388629e5a56ac6f17e2eef957b.solution');
CALL cheatSheetCreate('dc89383763c68cba0aaa1c6f3fd4c17e9d49a805', 'dc89383763c68cba0aaa1c6f3fd4c17e9d49a805.solution');
-CALL cheatSheetCreate('5b461ebe2e5e2797740cb3e9c7e3f93449a93e3a', '5b461ebe2e5e2797740cb3e9c7e3f93449a93e3a.solution');
CALL cheatSheetCreate('c685f8102ae0128c2ab342df64699bb8209a0839', 'c685f8102ae0128c2ab342df64699bb8209a0839.solution');
CALL cheatSheetCreate('d7eaeaa1cc4f218abd86d14eefa183a0f8eb6298', 'd7eaeaa1cc4f218abd86d14eefa183a0f8eb6298.solution');
CALL cheatSheetCreate('5ca9115f3279b9b9f3308eb6a59a4fcd374846d6', '5ca9115f3279b9b9f3308eb6a59a4fcd374846d6.solution');
-CALL cheatSheetCreate('6f5db377c28da4179bca1a43ede8d6bcf7bd322e', '6f5db377c28da4179bca1a43ede8d6bcf7bd322e.solution');
CALL cheatSheetCreate('f02ce6bcd0a822d245433533997eaf44379065f4', 'f02ce6bcd0a822d245433533997eaf44379065f4.solution');
CALL cheatSheetCreate('df2ac757cc135dcb8ce5ea01f677c74f04b446d6', 'df2ac757cc135dcb8ce5ea01f677c74f04b446d6.solution');
CALL cheatSheetCreate('08b3dffd4b837ebe53d52e53b5bbbabf4a4ca9ae', '08b3dffd4b837ebe53d52e53b5bbbabf4a4ca9ae.solution');
+-- New mobile modules for the unified Security Shepherd Android app
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('0f40ae03b9339cb88fbd834213ee1c597791274a', 'Mobile Insecure Authorization', 'mobile.insecure.authorization', 'lesson', 'Mobile Insecure Authorization', 'mobile.insecure.authorization', 'Purple_Boots_Dance_On_Ice', '749ea8a43debf521810ce13efe8f8f8a92318f51094957fc010507e4083d3dc7', 'open', '105', '35', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('a76d11ebd575aecfba5d69441cbd90c95e8abe31', 'Mobile Insecure Communication', 'mobile.insecure.communication', 'lesson', 'Mobile Insecure Communication', 'mobile.insecure.communication', 'Green_Socks_Float_Past_Reeds', '6a7a56237fbedee639dabbae9a3d6a0c069fdcf8dd5c89f45d8f0b0e2f44e7e2', 'open', '106', '35', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7', 'Mobile Insecure Communication Challenge', 'mobile.insecure.communication.challenge', 'challenge', 'Mobile Insecure Communication', 'mobile.insecure.communication', 'Empty_Wave_Breaks_On_Shore', 'd8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9', 'open', '107', '60', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('c85dad7f468a333e53edaca90a435528db76d118', 'Mobile Security Misconfiguration', 'mobile.security.misconfiguration', 'lesson', 'Mobile Security Misconfiguration', 'mobile.security.misconfiguration', 'Quiet_Frogs_Jump_On_Tuesday', '1d20d7c58bf71e8014840816e57bdca64ada0a6586a53a76ee23a30a06f75a80', 'open', '108', '35', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8', 'Mobile Security Misconfiguration Challenge 2', 'mobile.security.misconfiguration.challenge.2', 'challenge', 'Mobile Security Misconfiguration', 'mobile.security.misconfiguration', 'Faded_Map_Leads_Nowhere_Fast', 'e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0', 'open', '109', '65', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('708b76213e50409e138fc68eba81ed7ec8fccf08', 'Mobile Input Validation', 'mobile.input.validation', 'lesson', 'Mobile Input Validation', 'mobile.input.validation', 'Seven_Clouds_Chase_A_Kite', '65ba38b8f569f52f0648d7fca0c25954827e9f8dd658928f302f30914d778e62', 'open', '110', '35', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('952c4c3785d8bd8d51bb0d0161c3f6997dd01863', 'Mobile Privacy Controls', 'mobile.privacy.controls', 'lesson', 'Mobile Privacy Controls', 'mobile.privacy.controls', 'Silver_Moths_Dream_Of_Keys', '6607de796e7f642ca1c328f700335c8fe5804bc189374c4150c8e3365ccfe038', 'open', '111', '35', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('b9c1f4a7e2d3b5c8f6e1a4d7c2b9f3e8a5d1c6b3', 'Mobile Supply Chain', 'mobile.supply.chain', 'lesson', 'Mobile Supply Chain', 'mobile.supply.chain', 'Yellow_Lamp_Hums_All_Night', 'f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2', 'open', '112', '35', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('3385d879b0da97597e16e5c8a7511a6ec331d1d9', 'Mobile Insufficient Cryptography', 'mobile.insufficient.cryptography', 'lesson', 'Mobile Insufficient Cryptography', 'mobile.insufficient.cryptography', 'Dusty_Mirror_Speaks_In_Whispers', 'dbf3915eaaf531b9a95b0404b9a7186fb6dafe67ba96dc56d91f09e3f5edeea6', 'open', '113', '35', 0, 0);
+INSERT INTO modules (`moduleId`, `moduleName`, `moduleNameLangPointer`, `moduleType`, `moduleCategory`, `moduleCategoryLangPointer`, `moduleResult`, `moduleHash`, `moduleStatus`, `incrementalRank`, `scoreValue`, `hardcodedKey`, `isUnsafe`) VALUES ('c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2', 'Mobile Insufficient Cryptography Challenge', 'mobile.insufficient.cryptography.challenge', 'challenge', 'Mobile Insufficient Cryptography', 'mobile.insufficient.cryptography', 'Blind_Fish_Swims_In_Salt', 'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4', 'open', '114', '60', 0, 0);
+CALL cheatSheetCreate('0f40ae03b9339cb88fbd834213ee1c597791274a', '0f40ae03b9339cb88fbd834213ee1c597791274a.solution');
+CALL cheatSheetCreate('a76d11ebd575aecfba5d69441cbd90c95e8abe31', 'a76d11ebd575aecfba5d69441cbd90c95e8abe31.solution');
+CALL cheatSheetCreate('c85dad7f468a333e53edaca90a435528db76d118', 'c85dad7f468a333e53edaca90a435528db76d118.solution');
+CALL cheatSheetCreate('708b76213e50409e138fc68eba81ed7ec8fccf08', '708b76213e50409e138fc68eba81ed7ec8fccf08.solution');
+CALL cheatSheetCreate('952c4c3785d8bd8d51bb0d0161c3f6997dd01863', '952c4c3785d8bd8d51bb0d0161c3f6997dd01863.solution');
+CALL cheatSheetCreate('b9c1f4a7e2d3b5c8f6e1a4d7c2b9f3e8a5d1c6b3', 'b9c1f4a7e2d3b5c8f6e1a4d7c2b9f3e8a5d1c6b3.solution');
+CALL cheatSheetCreate('3385d879b0da97597e16e5c8a7511a6ec331d1d9', '3385d879b0da97597e16e5c8a7511a6ec331d1d9.solution');
+CALL cheatSheetCreate('c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2', 'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2.solution');
+
COMMIT;
-- Default admin user
diff --git a/src/main/resources/i18n/challenges/mobile/inputValidation/inputValidationStrings.properties b/src/main/resources/i18n/challenges/mobile/inputValidation/inputValidationStrings.properties
new file mode 100644
index 000000000..6e80d6fcf
--- /dev/null
+++ b/src/main/resources/i18n/challenges/mobile/inputValidation/inputValidationStrings.properties
@@ -0,0 +1,2 @@
+challenge1.challengeName = Mobile XSS WebView Challenge
+challenge1.para1 = The app loads user-supplied input into a WebView without sanitization. Craft a JavaScript payload that reads the flag variable from the page's JavaScript context and displays it. A successful XSS injection will reveal the flag.
diff --git a/src/main/resources/i18n/challenges/mobile/insecureComm/insecureCommStrings.properties b/src/main/resources/i18n/challenges/mobile/insecureComm/insecureCommStrings.properties
new file mode 100644
index 000000000..f1133bb87
--- /dev/null
+++ b/src/main/resources/i18n/challenges/mobile/insecureComm/insecureCommStrings.properties
@@ -0,0 +1,2 @@
+challenge1.challengeName = Mobile Insecure Communication 1
+challenge1.para1 = The app for this challenge makes an HTTP request to the server. The flag is transmitted as a custom header in the request body. Configure a proxy (such as Burp Suite or mitmproxy) between your device and the network, then trigger the request inside the app to intercept the flag in transit.
diff --git a/src/main/resources/i18n/challenges/mobile/insufficientCrypto/insufficientCryptoStrings.properties b/src/main/resources/i18n/challenges/mobile/insufficientCrypto/insufficientCryptoStrings.properties
new file mode 100644
index 000000000..9b300a481
--- /dev/null
+++ b/src/main/resources/i18n/challenges/mobile/insufficientCrypto/insufficientCryptoStrings.properties
@@ -0,0 +1,2 @@
+challenge1.challengeName = Mobile Insufficient Cryptography Challenge
+challenge1.para1 = The MobileShepherd app encrypts a secret value using a weak DES cipher with a short, guessable key. Intercept the encrypted data and crack the key to reveal the hidden flag.
diff --git a/src/main/resources/i18n/challenges/mobile/securityMisconfig/securityMisconfigStrings.properties b/src/main/resources/i18n/challenges/mobile/securityMisconfig/securityMisconfigStrings.properties
new file mode 100644
index 000000000..5b512ec0a
--- /dev/null
+++ b/src/main/resources/i18n/challenges/mobile/securityMisconfig/securityMisconfigStrings.properties
@@ -0,0 +1,2 @@
+challenge1.challengeName = Mobile Security Misconfiguration 2
+challenge1.para1 = The app stores the flag in Android SharedPreferences without any encryption. Use ADB backup, a root file explorer, or the adb shell run-as command to extract the app's SharedPreferences XML file and read the flag stored under the key "secret_flag".
diff --git a/src/main/resources/i18n/cheatsheets/solutions.properties b/src/main/resources/i18n/cheatsheets/solutions.properties
index cae84456c..14b9bb53e 100644
--- a/src/main/resources/i18n/cheatsheets/solutions.properties
+++ b/src/main/resources/i18n/cheatsheets/solutions.properties
@@ -35,19 +35,11 @@ c7ac1e05faa2d4b1016cfcc726e0689419662784.solution = View the source of this chal
b3cfd5890649e6815a1c7107cc41d17c82826cfa.solution = There are a number of ways to defeat the crypto and get the encryption key in this challenge. The quickest way is to submit base64 encoded spaces. The crypto XOR's the spaces with the key and returns the resultant 'cipher text which is the encryption key.
ced925f8357a17cfe3225c6236df0f681b2447c4.solution = Users must discover the session id for this sub application is very weak. The default session ID for a guest will be 00000001 base64'd twice. The admin's session ID is TURBd01EQXdNREF3TURBd01EQXdPUT09 (0000000000000009 when decoded).
c6841bcc326c4bad3a23cd4fa6391eb9bdb146ed.solution = This challenge does not require a solution to be formed in XHTML to be detected. One way to pass this challenge is to submit the following; http://""onmouseover=alert(123);//
-53a53a66cb3bf3e4c665c442425ca90e29536edd.solution = The Admin credentials are stored in plain text in a db file. Go to /data/data/insecuredata/databases and run the following command " Strings Members" or "cat Members". To get to root CLI on the VM press alt F1.
-307f78f18fd6a87e50ed6705231a9f24cd582574.solution = The Admin credentials are encoded in a db file. Go to /data/data/insecuredata1/databases and cat the Users.db file. Burp has a decoder which will reveal the key, alternatively there are online tools which can also do this
-da3de2e556494a9c2fb7308a98454cf55f3a4911.solution = The Admin credentials are hashed (but not salted) in a db file. Go to /data/data/insecuredata2/databases and cat the db file called Password.db. The key is a password which has been hashed using MD5, there are online tools which will attempt to crack this hashed value.
+53a53a66cb3bf3e4c665c442425ca90e29536edd.solution = The app stores a SQLite database named Members in its private data directory. The Admin user's password (the flag) is stored as plaintext. Connect the device via ADB and run: adb shell "run-as org.owasp.mobileshepherd sqlite3 /data/data/org.owasp.mobileshepherd/databases/Members 'SELECT password FROM Members WHERE name=\"Admin\"'" to retrieve the flag.
+307f78f18fd6a87e50ed6705231a9f24cd582574.solution = The Admin credentials are Base64-encoded in the app's private SQLite database. Connect the device via ADB and run: adb shell "run-as org.owasp.mobileshepherd sqlite3 /data/data/org.owasp.mobileshepherd/databases/Users.db 'SELECT * FROM users'" then decode the encoded value using Burp Suite's Decoder tab or any online Base64 decoder to obtain the flag.
335440fef02d19259254ed88293b62f31cccdd41.solution = The Login is vulnerable to SQL injection, Admin ' OR 1 = 1 ; -- will work in the username field and anything in the password field (So a blank field error does not occur).
a3f7ffd0f9c3d15564428d4df0b91bd927e4e5e4.solution = The login is vulnerable to SQL injection however, some input is being filtered. OR + 1 = 1 will be filtering into spaces of comments. anyOtherValue=anyOtherValue will work as well as 0r.
e635fce334aa61fdaa459c21c286d6332eddcdd3.solution = The login is vulnerable to SQL injection however there is filtering in place, to get an OR in the statement use OORR, to get a comment use -OR-. So the following statement should work: Admin ' oorr 'a' = 'a' ; -or-
-ef6496892b8e48ac2f349cdd7c8ecb889fc982af.solution = The chat has not been encrypted but encoded using hex, this can be decoded using burp or the following site:http://www.asciitohex.com/
-3f010a976bcbd6a37fba4a10e4a057acc80bdc09.solution = The chat has been encrypted using DES. The same key is used every time and the key is stored insecurely within the app package.
-63bc4811a2e72a7c833962e5d47a41251cd90de3.solution = The chat has been encrypted using AES (with CBC mode). Multiple keys are used this time but keys are stored insecurely on the App. key 1 decrypts message 1, key 2 decrypts message 2 and so forth.
-2ab09c0c18470ae5f87d219d019a1f603e66f944.solution = The key is stored in the source code of the App, get dex2jar and use it to turn the apk to a jar file, then open the jar and find the key in the main class.
-f16bf2ab1c1bf400d36330f91e9ac6045edcd003.solution = The key is stored in the source code of the App, get dex2jar and use it to turn the apk to a jar file, then open the jar. The key is present within a conditional statement in the class called Triangle.
-9e46e3c8bde42dc16b9131c0547eedbf265e8f16.solution = The key is not present in the code, however a check for the key is. This can be reverse engineered and the code extracted and run as a java class. Running this java class after changing the check to print the key will reveal it.
-1506f22cd73d14d8a73e0ee32006f35d4f234799.solution = Logs are stored insecurely on the App. These contain the key. The logs can be found in a directory called "files" within the app package in the data/data directory. Every time the app is interacted with, new logs are generated.
ed732e695b85baca21d80966306a9ab5ec37477f.solution = In this challenge you must craft a HTTP request to reset an admin accounts password. The HTTP request is described in the javascript contained in the challenge page (The last function in the script). The token value in this request must be a base 64 encoded date time value such as the following;
Thu Aug 28 18:48:10 BST 2014
The token value must be less than 10 minutes from the servers time.
cfbf7b915ee56508ad46ab79878f37fd9afe0d27.solution = To complete this challenge a user must craft a CSRF attack that sends a POST request, to the request described in the challenge write up, with their CSRF token. This CSRF Token will work on any user.
9294ba32bdbd680e3260a0315cd98bf6ce8b69bd.solution = The first step in completing this challenge is to get an admin user's email address. Try to sign in as 'root' or 'superuser' to get one. To complete this challenge a user must use SQL Injection in the email Parameter in the GET request to the SecretQuestion servlet. The following email submission will achieve the response of the users secret answer (This example is URL Encoded)
You can then use this answer along with a user email address to complete the level.
@@ -55,12 +47,10 @@ cfbf7b915ee56508ad46ab79878f37fd9afe0d27.solution = To complete this challenge a
145111e80400e4fd48bd3aa5aca382e9c5640793.solution = To complete this challenge a user must deobfusticate the javascript found in /couponCheck.js and extract the relevent cryptoinformation to manually decrypt a javascript array of encrypted coupons, or to manipulate the javascript so that it returns the decrypted coupons. The "des" function call reveals the Encryption key and the padding used by the algorithm. The "des" function in the javascript can be changed to decryption mode by changing the boolean argument to negative. The text to be decoded needs to be taken from the array of encrypted coupon codes. To recover the coupon code for free oranges you can run this command from your browsers console when opened in the context of the level's JSP page; des(chars_from_hex('0ba950d08830c8079bded71b852934453db8f4ffff1f5842'), chars_from_hex(bits[6]), 0, chars_from_hex('821fd38b9a7c0247') ? 1 : 0, chars_from_hex('821fd38b9a7c0247'));
adc845f9624716eefabcc90d172bab4096fa2ac4.solution = To complete this challenge, a SQL Injection Flaw must be exploited to learn the name of the super admin in a request described in the module's javascript. This function is not used by the presented HTML. You must manually craft this request and include an injection attack through the BASE64 encoded cookie named 'currentPerson'. a simple "or"1"!="0 vector will work. Take the super admin's name and submit it encoded for BASE64 as the currentPerson cookie value in the request that is submitted when the Admin button is clicked. This will return the result key for the challenge.
64070f5aec0593962a29a141110b9239d73cd7b3.solution = To complete this challenge, a SQL injection flaw must be exploited. The vulnerable paramater is 'subUserEmail'. It must be mostly well formed as an email address to get past the validation process. The following vector, which is URL encoded, will sign the user in as user 1.
'or'1'='1'union%0aselect%0auserName%0afrom%0ausers%0awhere''!='%40v
-1e3c02ad49fa9a9e396a3b268d7da8f0b647d8f9.solution = To complete this challenge, connect the android debug bridge to the VM and run adb logcat –d \ to dump logs to a text file. Trigger the key log by pressing the lotto button
-f40b0cd5d45327c9426675313f581cf70c7c7c28.solution = To complete this challenge, start the app, go to the command line of the VM using ALT F1 and then navigate to /sdcard/, pictorial logs are places there. Connect adb to the device and run the adb pull command on the logs.
-ba6e65e4881c8499b5e53eb33b5be6b5d0f1fb2c.solution = To complete this challenge, start the app, and login to get the key. you must login with an auth code. the code must be odd, must contain the numbers 2 and 4 and must be six digits long. previous codes may show up in a suggestion when typing the code in which will reveal this pattern.
-52885a3db5b09adc24f38bc453fe348f850649b3.solution = To complete this challenge, find jarsigner which comes with the jdk and in a command line run the following: jarsigner -verify -verbose -certs ReverseEngineer2.apk.
-3b1af0ad239325bf494c6e606585320b31612e72.solution = To complete this challenge, use adb pull to grab the key file and the key.db file from the app's /data/data/ directory. With the db password: Pa88w0rd1234 decrypt the database to get the key to the level. This will either require a small amount of coding or you can download and build sqlcipher. Finally there is an App on the playstore which can be used called SQLCipher Decrypt.
-0cdd1549e7c74084d7059ce748b93ef657b44457.solution = To complete this challenge, you need to login to the App. The password reset function rquires two answers which can be gathered from the logs on the App. The answers are chicken and meade. This will reset the password to a six digit code and allow you to login and view the key.
+ba6e65e4881c8499b5e53eb33b5be6b5d0f1fb2c.solution = The challenge writes personal details to application logs on startup. Connect the device via ADB and run adb logcat -s PoorAuthChallenge. The logs reveal that the user's name is Jack Meade and that he ate chicken. Use these answers in the password reset function (answer 1: chicken, answer 2: Meade). This resets the password, allowing login as Jack to retrieve the flag.
+52885a3db5b09adc24f38bc453fe348f850649b3.solution = Decompile the Security Shepherd APK using jadx or apktool. Locate the class ReverseEngineering1Model inside the challenges.reverseengineering package. The flag is stored as a Base64-encoded constant named ENCODED_SECRET. Decode it with any Base64 decoder (e.g. CyberChef or Burp Decoder) to reveal the flag.
+2ab09c0c18470ae5f87d219d019a1f603e66f944.solution = The Reverse Engineering lesson (LessonFragment) displays real device information - serial number, build fingerprint, manufacturer, brand and SDK version - gathered at runtime. Decompile the Security Shepherd APK using jadx or apktool and examine LessonFragment and LessonModel to understand how device data is enumerated. The lesson flag is issued by the Security Shepherd server once the lesson is reviewed on the platform page.
+0cdd1549e7c74084d7059ce748b93ef657b44457.solution = The lesson verifies a PIN by comparing its SHA-256 hash against a constant hardcoded in PoorAuthLessonFragment. Decompile the APK using jadx or apktool and locate the HARDCODED_PIN_HASH field. The hash 481f6cc0511143ccdd7e2d1b1b94faf0a700a8b49cd13922a70b5ae28acaa8c5 is the SHA-256 of the PIN 654321. Enter the PIN 654321 in the app to authenticate and reveal the flag.
368491877a0318e9a774ba5d648c33cb0165ba1e.solution = This challenge requires a bit of thinking to complete organically. First you must find some admin email addresses. The login function will return them when valid usernames are submitted. Try using root or superuser with any password. Use the email address in the secret question function to get that user's Secret Question. The secret question for each user is 'What is your favourite flower?'. There are only so many flowers. Any of the following flowers are valid answers. Root's favourite flower is 'Franklin Tree'.
Valid answers: Jade Vine, Corpse Flower, Gibraltar Campion, Franklin Tree, Middlemist Red, Chocolate Cosmos or Ghost Orchid
6be5de81223cc1b38b6e427cc44f8b6a28d2bc96.solution = The shopping cart application does not validate the number of items you are buying. Set the orange amount to 1, apple amount to 0, banana amount to 0 and the pineapple amount to -101.
3b14ca3c8f9b90c9b2c8cd1fba9fa67add1272a3.solution = The shopping cart application only ensures that the amount of items bought is a positive number. By buying 999295724 oranges, the total cost integer value will overflow and enter a negative state.
@@ -70,9 +60,20 @@ fcc2558e0a23b8420e173cf8029876cb887408d3.solution = To complete this challenge,
6158a695f20f9286d5f12ff3f4d42678f4a9740c.solution = To complete this challenge, you must be able to capture the traffic of another user. The simplest way to simulate this would be to create a second user account and open it in a separate browser and open this challenge. You could then just steal the cookie straight from the browser. To demo how to solve this as expected you would actually open Wireshark and record the 2nd user opening the challenge. Filter the network capture for ip.dst == ShepherdInstanceIp, and find the unencrypted HTTP packet. Right click it and select 'Follow TCP stream'. You'll see the cookie in that dialog.
Once you have the token collected, in your original browser, click the button and intercept the request with a Proxy. Replace your cookie value with the one you collected from another user.
de626470273c01388629e5a56ac6f17e2eef957b.solution = To complete this challenge you must first register an account. The account must have a unique name. The next step is to click the refresh balance button. Capture this request, and replay it with different account numbers until you find one with cash. If you are the first person to attempt this challenge, the account number 1 should have 10 million in it. Take note of the account number that has cash. Now fill out the 'Transfer Funds' form with any data. Capture that request and change the receiver account number parameter to the value the sender account number parameter is currently equal to (This is your account number), change the sender account number to the identifier you noted earlier and set the transfer amount to as high as possible (must be some money left in account to work). Keep doing this until your account has more than 5 million in it. Then open the level again or sign in / out of the account to get the result key
dc89383763c68cba0aaa1c6f3fd4c17e9d49a805.solution = The following attack vectors will expose the result key over two queries.
Step One: test' AND (SELECT 7303 FROM(SELECT COUNT(*),CONCAT(0x716b6a7671,(SELECT MID((IFNULL(CAST(comment AS CHAR),0x20)),1,50) FROM sqlchalstoredproc.customers ORDER BY customerId LIMIT 2,1),0x71786b7a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND 'hdTL'='hdTL
This will return an error revealing the first part of the key in the message with qxkzq1 added to the end for padding. remove those characters and record the rest of the key revealed.
Step Two: test' AND (SELECT 9441 FROM(SELECT COUNT(*),CONCAT(0x716b6a7671,(SELECT MID((IFNULL(CAST(comment AS CHAR),0x20)),51,50) FROM sqlchalstoredproc.customers ORDER BY customerId LIMIT 2,1),0x71786b7a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND 'ilGf'='ilGf
This will reveal the second part of the key, padded with qkjvq at the start and qxkzq1 at the end. Remove the padding and add the rest to the previously revealed part of the result key. That is the key to solve this challenge.
-5b461ebe2e5e2797740cb3e9c7e3f93449a93e3a.solution = Connect to the device via adb and run the following command - adb shell content query --uri content://com.somewhere.hidden.SecretProvider/data
c685f8102ae0128c2ab342df64699bb8209a0839.solution = One way to exploit this security risk is to escape the leading backslash that is added before apostrophes with another backslash. The following attack vector will solve the level;
\'or"1"="1"; -- 
d7eaeaa1cc4f218abd86d14eefa183a0f8eb6298.solution = The database in this level utilises MongoDb which does NOT use SQL. Queries in MongoDb are done through JavaScript. You need to stop the request in an HTTP proxy and use the following JavaScript to exploit the vulnerability: ';return(true);var a='a
f02ce6bcd0a822d245433533997eaf44379065f4.solution = This challenge is difficult to exploit and heavily depends on your user name. See this github ticket for more details. This was a vulnerability in Security Shepherd that was fixed
df2ac757cc135dcb8ce5ea01f677c74f04b446d6.solution = To exploit this take Example 1, replace file:///etc/shadow with file:///usr/local/tomcat/briefcase.pulp and also replace <foo>&xxe;</foo> with <email>&xxe;</email>
-08b3dffd4b837ebe53d52e53b5bbbabf4a4ca9ae.solution = To exploit this view the source of the iframe where you'll see two functions for sending a request - leFormNew and leFormOld. The new one is for JSON and the old one uses XML where the developers left this one in place and can be exploited through XML injection. It is exploited the very same way as the XXE lesson.
\ No newline at end of file
+08b3dffd4b837ebe53d52e53b5bbbabf4a4ca9ae.solution = To exploit this view the source of the iframe where you'll see two functions for sending a request - leFormNew and leFormOld. The new one is for JSON and the old one uses XML where the developers left this one in place and can be exploited through XML injection. It is exploited the very same way as the XXE lesson.
+# --- Mobile module cheatsheets added/updated for integrated Security Shepherd app ---
+3385d879b0da97597e16e5c8a7511a6ec331d1d9.solution = The lesson presents four DES-encrypted secrets, each with a weak guessable single-word key shown as a hint. Secret 4 (hint: Material for warmth) is encrypted with the key wool. Use CyberChef (Algorithm: DES, Mode: ECB, Input: Base64) with key wool padded with null bytes to 8 bytes to decrypt the ciphertext of Secret 4. The decrypted plaintext is the flag. Tap Secret 4 in the app and enter the decrypted text to complete the lesson.
+c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2.solution = The challenge encrypts a secret flag with DES using a weak hardcoded key. Decompile the Security Shepherd APK using jadx or apktool and locate InsufficientCryptoChallengeFragment to find the encrypted ciphertext and the key. Use CyberChef (Algorithm: DES, Mode: ECB, Input: Base64) with the extracted key to decrypt the ciphertext and retrieve the flag.
+0f40ae03b9339cb88fbd834213ee1c597791274a.solution = The app performs authorization checks based on a user_role value stored client-side in SharedPreferences. Log in with the demo credentials (username: testuser, password: password123). The app stores user_role as user in UserSession.xml. Modify it via ADB: adb shell "run-as org.owasp.mobileshepherd sed -i 's/value=\"user\"/value=\"admin\"/g' /data/data/org.owasp.mobileshepherd/shared_prefs/UserSession.xml". Return to the app and tap Access Admin Panel to reveal the flag.
+a76d11ebd575aecfba5d69441cbd90c95e8abe31.solution = The lesson sends a real HTTP (not HTTPS) GET request to the Security Shepherd server, transmitting the session cookie and username in cleartext. Configure a network proxy such as Burp Suite or mitmproxy on the same Wi-Fi network as the device and route device traffic through it. Press Send HTTP Request in the app and observe the plaintext Cookie header in the intercepted request - any on-path observer can read these credentials. The flag is confirmed via the Security Shepherd platform page.
+d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7.solution = The challenge simulates an app making five network requests, only some of which are over unencrypted HTTP. Connect the device via ADB and run: adb logcat -s AppNetworkMonitor. Press Start App in the challenge to trigger the traffic. Read through the captured requests in logcat. Request #2 (User Session Endpoint) sends an X-Session-Token header over HTTP/1.1 in plaintext — copy that token value and submit it as the flag.
+c85dad7f468a333e53edaca90a435528db76d118.solution = The app is built in debug mode (android:debuggable="true" in AndroidManifest.xml). When the Check Configuration button is pressed, the app logs the flag to Android logcat under the tag SecurityMisconfig. Connect the device via ADB, run adb logcat -s SecurityMisconfig, and press Check Configuration in the app. Read the DEBUG_SECRET value from the log output, then enter it in the flag submission card.
+e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8.solution = The challenge app has android:allowBackup="true" enabled, so the flag stored in SharedPreferences is included in ADB backups. Tap the challenge to trigger the flag being stored, then run: adb backup -f backup.ab -noapk org.owasp.mobileshepherd. Unpack using Android Backup Extractor: java -jar abe.jar unpack backup.ab backup.tar, then tar xf backup.tar. Open apps/org.owasp.mobileshepherd/sp/BackupChallengePrefs.xml and read the secret_flag value.
+708b76213e50409e138fc68eba81ed7ec8fccf08.solution = The app validates deep link URLs using a weak contains() check, accepting any URL that contains example.com or trusted-site.com anywhere in the string. Bypass the validation by entering a URL that includes a trusted domain as a query parameter while targeting the hidden admin panel. For example: https://admin.internal?ref=example.com passes validation (example.com is present) while loading the admin content, which reveals the flag.
+f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9.solution = The challenge embeds user-provided name and bio fields directly into a WebView HTML document without HTML encoding, and places the flag in a JavaScript variable secretFlag. Inject a script payload in the Name field to call revealFlag(): enter or . Tap Update Profile to render the injected HTML. The flag appears in the green banner when the function executes.
+952c4c3785d8bd8d51bb0d0161c3f6997dd01863.solution = The lesson saves a JPEG image with the flag embedded in the EXIF ImageDescription metadata field. Tap Load Sample Image to generate the file, then pull it from the device: adb shell "run-as org.owasp.mobileshepherd cp /data/data/org.owasp.mobileshepherd/files/privacy_sample.jpg /sdcard/privacy_sample.jpg" followed by adb pull /sdcard/privacy_sample.jpg. Inspect the EXIF metadata using ExifTool (exiftool privacy_sample.jpg) and read the Image Description field to find the flag.
+b9c1f4a7e2d3b5c8f6e1a4d7c2b9f3e8a5d1c6b3.solution = The lesson simulates a vulnerable third-party library (androidx.exifinterface:exifinterface:1.3.7) that leaks debug configuration data through application logs. Connect the device via ADB and run adb logcat -s SupplyChainLesson. When the app loads, the library logs: Debug processor key: exif_debug_processor_key_1337. This demonstrates a supply chain risk where a dependency exposes sensitive internal keys. Enter the key value found in logcat to complete the lesson.
diff --git a/src/main/resources/i18n/lessons/m_input_validation/65ba38b8f569f52f0648d7fca0c25954827e9f8dd658928f302f30914d778e62.properties b/src/main/resources/i18n/lessons/m_input_validation/65ba38b8f569f52f0648d7fca0c25954827e9f8dd658928f302f30914d778e62.properties
new file mode 100644
index 000000000..14e5ed5b7
--- /dev/null
+++ b/src/main/resources/i18n/lessons/m_input_validation/65ba38b8f569f52f0648d7fca0c25954827e9f8dd658928f302f30914d778e62.properties
@@ -0,0 +1,6 @@
+title.question.m_input_validation = What is Mobile Input Validation?
+
+paragraph.info = Insufficient input validation in mobile apps can lead to injection attacks, including Cross-Site Scripting (XSS) in WebView components. When a WebView renders user-supplied HTML or JavaScript without sanitization, an attacker can inject malicious scripts. In this lesson, experiment with the WebView input field and observe how unescaped content is rendered. The flag is revealed once you successfully exploit the XSS vulnerability.
+
+button.hideIntro = Hide Lesson Introduction
+button.showIntro = Show Lesson Introduction
diff --git a/src/main/resources/i18n/lessons/m_insecure_authorization/749ea8a43debf521810ce13efe8f8f8a92318f51094957fc010507e4083d3dc7.properties b/src/main/resources/i18n/lessons/m_insecure_authorization/749ea8a43debf521810ce13efe8f8f8a92318f51094957fc010507e4083d3dc7.properties
new file mode 100644
index 000000000..7e8b83fed
--- /dev/null
+++ b/src/main/resources/i18n/lessons/m_insecure_authorization/749ea8a43debf521810ce13efe8f8f8a92318f51094957fc010507e4083d3dc7.properties
@@ -0,0 +1,6 @@
+title.question.m_insecure_authorization = What is Mobile Insecure Authorization?
+
+paragraph.info = Insecure Authorization occurs when a mobile app does not properly enforce access controls, allowing users to access resources or functions they should not be permitted to use. A common example is storing a user's role or privilege level in SharedPreferences or local storage where it can be easily modified by an attacker. In this lesson the app grants admin access based on a SharedPreferences flag. You can escalate your privileges by modifying this value, revealing the flag.
+
+button.hideIntro = Hide Lesson Introduction
+button.showIntro = Show Lesson Introduction
diff --git a/src/main/resources/i18n/moduleGenerics/moduleNames.properties b/src/main/resources/i18n/moduleGenerics/moduleNames.properties
index 8bd1c433e..908dbb314 100644
--- a/src/main/resources/i18n/moduleGenerics/moduleNames.properties
+++ b/src/main/resources/i18n/moduleGenerics/moduleNames.properties
@@ -1,13 +1,7 @@
-broken.crypto = Broken Crypto
-broken.crypto.1 = Broken Crypto 1
-broken.crypto.2 = Broken Crypto 2
-broken.crypto.3 = Broken Crypto 3
broken.session.management = Broken Session Management
client.side.injection = Client Side Injection
client.side.injection.1 = Client Side Injection 1
client.side.injection.2 = Client Side Injection 2
-content.provider.leakage = Content Provider Leakage
-content.provider.leakage.1 = Content Provider Leakage 1
cross.site.request.forgery = Cross Site Request Forgery
cross.site.scripting = Cross Site Scripting
cross.site.scripting.1 = Cross Site Scripting 1
@@ -36,8 +30,6 @@ insecure.cryptographic.storage.challenge.4 = Insecure Cryptographic Storage Chal
insecure.cryptographic.storage.home.made.key = Insecure Cryptographic Storage Home Made Keys
insecure.data.storage = Insecure Data Storage
insecure.data.storage.1 = Insecure Data Storage 1
-insecure.data.storage.2 = Insecure Data Storage 2
-insecure.data.storage.3 = Insecure Data Storage 3
insecure.direct.object.reference.bank = Insecure Direct Object Reference Bank
insecure.direct.object.reference.challenge.1 = Insecure Direct Object Reference Challenge 1
insecure.direct.object.reference.challenge.2 = Insecure Direct Object Reference Challenge 2
@@ -45,14 +37,11 @@ insecure.direct.object.references = Insecure Direct Object References
nosql.injection.one = NoSQL Injection One
poor.authentication = Poor Authentication
poor.authentication.1 = Poor Authentication 1
-poor.authentication.2 = Poor Authentication 2
poor.data.validation = Poor Data Validation
poor.data.validation.1 = Poor Data Validation 1
poor.data.validation.2 = Poor Data Validation 2
reverse.engineering = Reverse Engineering
reverse.engineering.1 = Reverse Engineering 1
-reverse.engineering.2 = Reverse Engineering 2
-reverse.engineering.3 = Reverse Engineering 3
security.misconfig.cookie.flag = Security Misconfig Cookie Flag
security.misconfiguration = Security Misconfiguration
session.management.challenge.1 = Session Management Challenge 1
@@ -73,27 +62,36 @@ sql.injection.6 = SQL Injection 6
sql.injection.7 = SQL Injection 7
sql.injection.escaping = SQL Injection Escaping
sql.injection.stored.procedure = SQL Injection Stored Procedure
-unintended.data.leakage = Unintended Data Leakage
-unintended.data.leakage.1 = Unintended Data Leakage 1
-unintended.data.leakage.2 = Unintended Data Leakage 2
-untrusted.input = Untrusted Input
unvalidated.redirects.and.forwards = Unvalidated Redirects and Forwards
xxe.lessons = XML External Entity (XXE) Injection
xxe.injection.1 = XXE Injection 1
+mobile.insecure.authorization = Mobile Insecure Authorization
+mobile.insecure.communication = Mobile Insecure Communication
+mobile.insecure.communication.challenge = Mobile Insecure Communication Challenge
+mobile.security.misconfiguration = Mobile Security Misconfiguration
+mobile.security.misconfiguration.challenge.2 = Mobile Security Misconfiguration Challenge 2
+mobile.input.validation = Mobile Input Validation
+mobile.privacy.controls = Mobile Privacy Controls
+mobile.supply.chain = Mobile Supply Chain
+mobile.insufficient.cryptography = Mobile Insufficient Cryptography
+mobile.insufficient.cryptography.challenge = Mobile Insufficient Cryptography Challenge
category.csrf = CSRF
category.failure.to.restrict.url.access = Failure to Restrict URL Access
category.injection = Injection
category.insecure.cryptographic.storage = Insecure Cryptographic Storage
category.insecure.direct.object.references = Insecure Direct Object References
-category.mobile.broken.crypto = Mobile Broken Crypto
-category.mobile.content.provider = Mobile Content Providers
-category.mobile.data.leakage = Mobile Data Leakage
category.mobile.injection = Mobile Injection
+category.mobile.insecure.authorization = Mobile Insecure Authorization
+category.mobile.insecure.communication = Mobile Insecure Communication
category.mobile.insecure.data.storage = Mobile Insecure Data Storage
+category.mobile.input.validation = Mobile Input Validation
+category.mobile.insufficient.cryptography = Mobile Insufficient Cryptography
category.mobile.poor.authentication = Mobile Poor Authentication
+category.mobile.privacy.controls = Mobile Privacy Controls
category.mobile.reverse.engineering = Mobile Reverse Engineering
-category.mobile.security.decisions.via.untrusted.input = Mobile Security Decisions via Untrusted Input
+category.mobile.security.misconfiguration = Mobile Security Misconfiguration
+category.mobile.supply.chain = Mobile Supply Chain
category.poor.data.validation = Poor Data Validation
category.security.misconfigurations = Security Misconfigurations
category.session.management = Session Management
diff --git a/src/main/webapp/challenges/11ccaf2f3b2aa4f88265b9cacb5e0ed26b11af978523e34528cf0bb9d32de851.jsp b/src/main/webapp/challenges/11ccaf2f3b2aa4f88265b9cacb5e0ed26b11af978523e34528cf0bb9d32de851.jsp
deleted file mode 100644
index a045b29bf..000000000
--- a/src/main/webapp/challenges/11ccaf2f3b2aa4f88265b9cacb5e0ed26b11af978523e34528cf0bb9d32de851.jsp
+++ /dev/null
@@ -1,101 +0,0 @@
-<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
- language="java" import="utils.*" errorPage=""%>
-<%@ page import="java.util.Locale, java.util.ResourceBundle"%>
-<%
-/**
- *
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Insecure Data Storage 3";
-//Alphanumeric Only
-String levelHash = "11ccaf2f3b2aa4f88265b9cacb5e0ed26b11af978523e34528cf0bb9d32de851";
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.insecureData.insecureDataStrings", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-//Used more than once translations
-String LevelName = bundle.getString("challenge3.challengeName");
-String paragraph1 = bundle.getString("challenge3.para1");
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-%>
-
-
-
-Security Shepherd - <%= levelName %>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Unintended Data Leakage 1";
-//Alphanumeric Only
-String levelHash = "517622a535ff89f7d90674862740b48f53aad7b41390fe46c6f324fee748d136";
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.unintendedDataLeakage.dataLeakage", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge1.challengeName");
-String paragraph1 = bundle.getString("challenge1.para1");
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-
-%>
-
-
-
-Security Shepherd - <%=LevelName%>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
-*/
-//No Quotes In level Name
-String levelName = "Mobile Reverse Engineering 2";
-//Alphanumeric Only
-String levelHash = "5bc811f9e744a71393a277c51bfd8fbb5469a60209b44fa3485c18794df4d5b1";
-
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.reverseEngineer.reverseEngineer", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge2.challengeName");
-String paragraph1 = bundle.getString("challenge2.para1");
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-
-%>
-
-
-
-Security Shepherd - <%=LevelName%>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Unintended Data Leakage 2";
-//Alphanumeric Only
-String levelHash = "85ceae7ec397c8f4448be51c33a634194bf5da440282227c15954bbdfb54f0c7";
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.unintendedDataLeakage.dataLeakage", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge2.challengeName");
-String paragraph1 = bundle.getString("challenge2.para1");
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-%>
-
-
-
-Security Shepherd - <%= levelName %>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Broken Crypto 1";
-//Alphanumeric Only
-String levelHash = "d2f8519f8264f9479f56165465590b499ceca941ab848805c00f5bf0a40c9717";
-
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.brokenCrypto.brokenCrypto", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge1.challengeName");
-String paragraph1 = bundle.getString("challenge1.para1");
-
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-%>
-
-
-
-Security Shepherd - <%= LevelName %>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Reverse Engineering 3";
-//Alphanumeric Only
-String levelHash = "dbae0baa3f71f196c4d2c6c984d45a6c1c635bf1b482dccfe32e9b01b69a042b.jsp";
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.reverseEngineer.reverseEngineer", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge3.challengeName");
-String paragraph1 = bundle.getString("challenge3.para1");
-
-//Level blurb can be written here in HTML OR go into the HTML body and write it there. Nobody will update this but you
-String levelBlurb = "";
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-%>
-
-
-
-Security Shepherd - <%=LevelName%>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Insecure Data Storage 2";
-//Alphanumeric Only
-String levelHash = "ec09515a304d2de1f552e961ab769967bdc75740ad2363803168b7907c794cd4";
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.insecureData.insecureDataStrings", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge2.challengeName");
-String paragraph1 = bundle.getString("challenge2.para1");
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-%>
-
-
-
-Security Shepherd - <%= levelName %>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Broken Crypto 3";
-//Alphanumeric Only
-String levelHash = "fb5c9ce0f5539b737e534fd317befff7427f6610ed626dfd43abf35295f106bc";
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.brokenCrypto.brokenCrypto", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge3.challengeName");
-String paragraph1 = bundle.getString("challenge3.para1");
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-%>
-
-
-
-Security Shepherd - <%= LevelName %>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-//No Quotes In level Name
-String levelName = "Mobile Broken Crypto 2";
-//Alphanumeric Only
-String levelHash = "fb5c9ce0f5539b737e534fd317befff7427f6610ed626dfd43abf35295f106bc";
-
-//Translation Stuff
-Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
-ResourceBundle bundle = ResourceBundle.getBundle("i18n.challenges.mobile.brokenCrypto.brokenCrypto", locale);
-ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
-
-//Used more than once translations
-String LevelName = bundle.getString("challenge2.challengeName");
-String paragraph1 = bundle.getString("challenge2.para1");
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-%>
-
-
-
-Security Shepherd - <%= LevelName %>
-
-
-
-
-
-
-
-
-
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-
-ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
-if (request.getSession() != null)
-{
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-
-%>
-
-
-
-Security Shepherd - <%= translatedLevelName %>
-
-
-
-
-
-
-
-
-
-
-
-<%
- }
- else
- {
- response.sendRedirect("../loggedOutSheep.html");
- }
-}
-else
-{
- response.sendRedirect("../loggedOutSheep.html");
-}
-%>
diff --git a/src/main/webapp/lessons/4d41997b5b81c88f7eb761c1975481c4ce397b80291d99307cfad69662277d39.jsp b/src/main/webapp/lessons/4d41997b5b81c88f7eb761c1975481c4ce397b80291d99307cfad69662277d39.jsp
deleted file mode 100644
index d8cf901c8..000000000
--- a/src/main/webapp/lessons/4d41997b5b81c88f7eb761c1975481c4ce397b80291d99307cfad69662277d39.jsp
+++ /dev/null
@@ -1,132 +0,0 @@
-<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
- language="java" import="utils.*" errorPage=""%>
-<%@ page import="java.util.Locale, java.util.ResourceBundle"%>
-
-<%
-
- //No Quotes In level Name
- String levelName = "Content Provider Leakage Lesson";
- //Alphanumeric Only
- String levelHash = "4d41997b5b81c88f7eb761c1975481c4ce397b80291d99307cfad69662277d39";
-
- //Translation Stuff
- Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
- ResourceBundle bundle = ResourceBundle.getBundle("i18n.lessons.m_content_provider_leakage." + levelHash, locale);
- ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
- //Used more than once translations
- String translatedLevelName = bundle.getString("title.question.content_provider_leak");
-
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
- if (request.getSession() != null)
- {
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-
- /*
- *
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-%>
-
-
-
-Security Shepherd - <%= translatedLevelName %>
-
-
-
-
-
-
-
-
-
-
-
-<%
- }
- else
- {
- response.sendRedirect("../loggedOutSheep.html");
- }
- }
- else
- {
- response.sendRedirect("../loggedOutSheep.html");
- }
-%>
\ No newline at end of file
diff --git a/src/main/webapp/lessons/911fa7f4232e096d6a74a0623842c4157e29b9bcc44e8a827be3bb7e58c9a212.jsp b/src/main/webapp/lessons/911fa7f4232e096d6a74a0623842c4157e29b9bcc44e8a827be3bb7e58c9a212.jsp
deleted file mode 100644
index f6b790fc8..000000000
--- a/src/main/webapp/lessons/911fa7f4232e096d6a74a0623842c4157e29b9bcc44e8a827be3bb7e58c9a212.jsp
+++ /dev/null
@@ -1,120 +0,0 @@
-<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
- language="java" import="utils.*" errorPage=""%>
-<%@ page import="java.util.Locale, java.util.ResourceBundle"%>
-
-<%
- //No Quotes In level Name
- String levelName = "Mobile Broken Crypto?";
- //Alphanumeric Only
- String levelHash = "911fa7f4232e096d6a74a0623842c4157e29b9bcc44e8a827be3bb7e58c9a212";
- //Translation Stuff
- Locale locale = new Locale(Validate.validateLanguage(request.getSession()));
- ResourceBundle bundle = ResourceBundle.getBundle("i18n.lessons.m_broken_crypto." + levelHash, locale);
- ResourceBundle mobile = ResourceBundle.getBundle("i18n.moduleGenerics.mobileGenericStrings", locale);
- //Used more than once translations
- String translatedLevelName = bundle.getString("title.question.mobile_broken_crypto");
-
- /**
- *
- * This file is part of the Security Shepherd Project.
- *
- * The Security Shepherd project is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * The Security Shepherd project is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with the Security Shepherd project. If not, see .
- *
- * @author Sean Duggan
- */
-
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " Accessed");
- if (request.getSession() != null)
- {
- HttpSession ses = request.getSession();
- //Getting CSRF Token from client
- Cookie tokenCookie = null;
- try
- {
- tokenCookie = Validate.getToken(request.getCookies());
- }
- catch(Exception htmlE)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName +".jsp: tokenCookie Error:" + htmlE.toString());
- }
- // validateSession ensures a valid session, and valid role credentials
- // If tokenCookie == null, then the page is not going to continue loading
- if (Validate.validateSession(ses) && tokenCookie != null)
- {
- ShepherdLogManager.logEvent(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), levelName + " has been accessed by " + ses.getAttribute("userName").toString(), ses.getAttribute("userName"));
-
-%>
-
-
-
-Security Shepherd - <%= translatedLevelName %>
-
-
-
-
-
-
-
-
-