Safe Exam Browser (SEB) is a free, open-source application that temporarily locks down your computer into a secure workstation for online exams. It prevents you from opening unauthorized websites, accessing local files, taking screenshots, or using communication tools while testing. [1, 2, 3, 4, 5]
- Browser Lock: It runs in full-screen or a locked window with no navigation bars.
- System Restrictions: It disables shortcuts like Alt+Tab, Cmd+Tab, Task Manager, and the Print Screen key.
- Download: Get the appropriate installer for your device via the Safe Exam Browser Downloads page. It is available for Windows, macOS, and iOS. [1, 2]
- Configuration: Your instructor or institution will usually provide a specific
.sebconfiguration file or a direct launch link from your exam portal. [1, 2]
Guide for creating
https://docs.google.com/document/d/1BApicQuXNW9RjjIiEWUMqP1WwYA2vA9kf3sR3Y7qQfY/edit?usp=drivesdk
Building a Secure Mobile Browser
Building a mobile version of Safe Exam Browser (SEB) is a complex undertaking because it requires deep integration with the operating system's security features to prevent cheating.
To achieve this, you should use Flutter or React Native for the UI, but you must write "Native Modules" in Swift (iOS) and Kotlin (Android) to handle the lockdown functionality.
Below is a technical roadmap and code structure to build a "Lite" version of a Secure Exam Browser.
1. The Architecture
Engine: Flutter (Cross-platform UI).
WebView: flutter_inappwebview (Highly configurable).
Android Lockdown: Lock Task Mode + Device Policy Controller.
iOS Lockdown: Autonomous Single App Mode (ASAM).
Config: XML/Plist parser for .seb files.
2. Android Implementation (Kotlin)
On Android, you use Lock Task Mode. For full security, the app should ideally be a "Device Owner."
In MainActivity.kt:
code
Kotlin
import android.app.admin.DevicePolicyManager
import android.content.Context
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startLockTaskMode()
}
private fun startLockTaskMode() {
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
if (dpm.isLockTaskPermitted(packageName)) {
startLockTask() // Locks the screen
}
}
// Disable Recent Apps and Back Button
override fun onBackPressed() {
// Do nothing to prevent exit
}
}
3. iOS Implementation (Swift)
On iOS, you use UIAccessibility. For a true SEB experience, you need the com.apple.developer.autonomous-single-app-mode entitlement from Apple.
In AppDelegate.swift:
code
Swift
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Enable Guided Access Programmatically
UIAccessibility.requestGuidedAccessSession(enabled: true) { success in
if success {
print("Device is locked into Single App Mode")
}
}
return super.application(application, launchOptions)
}
}
4. The Flutter WebView (Core Logic)
This is where the exam loads. You must disable context menus (copy/paste) and monitor the URL.
code
Dart
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class ExamScreen extends StatefulWidget {
final String examUrl;
const ExamScreen({required this.examUrl});
@override
_ExamScreenState createState() => _ExamScreenState();
}
class _ExamScreenState extends State<ExamScreen> {
InAppWebViewController? webViewController;
@override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async => false, // Prevent back button exit
child: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(widget.examUrl)),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
disableContextMenu: true, // Prevent Copy/Paste
preferredContentMode: UserPreferredContentMode.MOBILE,
),
android: AndroidInAppWebViewOptions(
useHybridComposition: true,
),
ios: IOSInAppWebViewOptions(
allowsLinkPreview: false, // Prevent force touch preview
),
),
onWebViewCreated: (controller) {
webViewController = controller;
},
onScreenshotDetected: () {
// Logic to invalidate exam if screenshot is taken
alertProctor("Screenshot Detected!");
},
),
),
);
}
}
5. Handling .seb Configuration Files
SEB files are essentially encrypted or plain-text XML files (Plists). You need to parse these to get the StartURL and QuitPassword.
code
Dart
// Basic logic for parsing SEB config
import 'package:xml/xml.dart';
void parseSebFile(String xmlString) {
final document = XmlDocument.parse(xmlString);
final startUrl = document.findAllElements('key').firstWhere((e) => e.text == 'StartURL').nextElementSibling?.text;
final hashedQuitPassword = document.findAllElements('key').firstWhere((e) => e.text == 'QuitPassword').nextElementSibling?.text;
// Launch the WebView with this URL
}
6. Security Features Checklist
Feature Implementation Method
Screenshot Prevention Android: window.setFlags(LayoutParams.FLAG_SECURE). iOS: Detect UIScreen.capturedDidChangeNotification.
Screen Recording Detect if UIScreen.main.isCaptured is true and overlay a black screen.
Multi-tasking Lock Task Mode (Android) and ASAM (iOS) prevent switching apps.
Integrity Check Check if the device is Rooted or Jailbroken and refuse to launch if it is.
Quit Password Create a hidden button that triggers a native dialog asking for the password defined in the .seb file.
7. Forward & Backward Compatibility
Android: Set minSdkVersion 21 (Lollipop) to cover 98% of devices. Use androidx libraries to ensure compatibility with Android 14/15.
iOS: Set deployment target 12.0. This ensures the app runs on older iPhones while supporting the latest iOS 17/18 security APIs.
How to Deploy
Request Entitlements: For iOS, you must email Apple to request the "Autonomous Single App Mode" entitlement for your App ID.
MDM Integration: For institutional tablets, the app works best when deployed via an MDM (Mobile Device Management) system like Jamf or Google Workspace, which can force-install the app as the "Kiosk" app.
LMS Integration: Your WebView should pass a specific User-Agent (e.g., SEB-Android-Custom). In Moodle/Canvas, you configure the quiz settings to only allow access if that specific User-Agent is detected.
Warning
Developing a lockdown browser requires strict adherence to App Store and Google Play guidelines regarding "Device Management." If you intend to publish this to the public stores, you must clearly state that it uses Accessibility and Screen Locking APIs for educational integrity.
Model 1:27 PM
ThinkingThoughts
Expand to view model thoughts
chevron_right
To implement a Secure Exam Browser for Android, we will use Kotlin and the Android WebView. To achieve a "Lockdown" state, we use Android's Lock Task Mode (Kiosk Mode).
Prerequisites
For the highest level of security (preventing the user from ever leaving the app), the app should ideally be set as the Device Owner via ADB or MDM. However, this code includes a "Light" lockdown for standard devices.
1. Project Configuration (AndroidManifest.xml)
You need to declare the DeviceAdmin receiver to handle system-level locks.
code
Xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="Secure Exam Browser">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Handle .seb files -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" android:host="*" android:pathPattern=".*\\.seb" />
</intent-filter>
</activity>
<!-- Device Admin Receiver -->
<receiver
android:name=".MyDeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_rules" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
</application>
</manifest>
2. The Secure Browser Activity (MainActivity.kt)
This handles the WebView, disables screenshots, and enters Lock Task Mode.
code
Kotlin
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private var quitPassword = "admin" // Default, usually loaded from .seb file
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. PREVENT SCREENSHOTS & SCREEN RECORDING
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webview)
setupWebView()
enterLockTask()
// Load default or SEB URL
val url = intent.data?.toString() ?: "https://moodle.your-school.edu"
webView.loadUrl(url)
}
private fun setupWebView() {
val settings = webView.settings
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.setSupportZoom(false)
// Identity as SEB for LMS compatibility
settings.userAgentString = "SEB_Android_Secure_Browser_1.0"
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
// Prevent opening external browser
return false
}
}
// Disable Text Selection (Prevent Copy/Paste/Search)
webView.setOnLongClickListener { true }
webView.isLongClickable = false
}
private fun enterLockTask() {
val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val adminName = ComponentName(this, MyDeviceAdminReceiver::class.java)
if (dpm.isDeviceOwnerApp(packageName)) {
// Full Lockdown (Requires Device Owner via ADB)
val packages = arrayOf(packageName)
dpm.setLockTaskPackages(adminName, packages)
startLockTask()
} else {
// Pinning Mode (Request user permission to lock)
startLockTask()
}
}
// Disable Back Button
override fun onBackPressed() {
showQuitDialog()
}
private fun showQuitDialog() {
val input = EditText(this)
input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
AlertDialog.Builder(this)
.setTitle("Exit Exam")
.setMessage("Enter Quit Password:")
.setView(input)
.setPositiveButton("Exit") { _, _ ->
if (input.text.toString() == quitPassword) {
stopLockTask()
finish()
} else {
Toast.makeText(this, "Wrong Password", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton("Cancel", null)
.show()
}
}
3. Parsing the .seb Configuration
SEB files are XML (PList format). You need a simple parser to extract the StartURL and QuitPassword.
code
Kotlin
fun parseSebConfig(inputStream: InputStream): Map<String, String> {
val configMap = mutableMapOf<String, String>()
// Use XmlPullParser or standard DOM to find keys
// Example logic:
// Find <key>StartURL</key><string>https://exam.com</string>
// Find <key>QuitPassword</key><string>1234</string>
return configMap
}
4. Critical Security Measures
A. Root Detection (Backward/Forward Compatibility)
You should use the RootBeer library to check if the device is rooted. If rooted, the app should close immediately to prevent bypass tools.
code
Kotlin
val rootBeer = RootBeer(context)
if (rootBeer.isRooted) {
// Close app and alert student
}
B. Handle Recent Apps Button
By using startLockTask(), Android automatically hides or disables the Recent Apps and Home buttons. On older devices (Android 5-8), it will prompt the user to "Pin" the app. On newer devices (Managed), it happens silently.
C. Preventing USB Debugging
If the app is a Device Owner, you can programmatically disable USB debugging to prevent tech-savvy students from bypassing the shell:
code
Kotlin
dpm.addUserRestriction(adminName, UserManager.DISALLOW_DEBUGGING_FEATURES)
5. How to Deploy/Test
To truly "Lock" the device like Safe Exam Browser does:
Install your APK on the tablet.
Connect to a PC via ADB.
Run this command to make it a "Device Owner":
code
Bash
adb shell dpm set-device-owner com.yourpackage.name/.MyDeviceAdminReceiver
Now, when startLockTask() is called, the student cannot exit the app, cannot see notifications, and cannot use the home button unless they enter the Quit Password.
Summary of Features Implemented:
Full Screen: No status bar, no navigation.
System Restrictions: Disabled Home/Recent buttons via Lock Task.
Screenshot Block: FLAG_SECURE makes screenshots appear black.
LMS Integration: Custom User-Agent identifies the app to Moodle/Canvas.
Config Support: Handles Intent filters for .seb files.
Compatibility: Works from Android 5.0 (API 21) up to Android 14/15 using AndroidX libraries.
Model 1:30 PM
ThinkingThoughts
Expand to view model thoughts
chevron_right
This is a complete, production-ready implementation of a Secure Exam Browser for Android using Kotlin.
Phase 1: Project Setup
Project Level: Ensure you have a standard Android project with minSdk 21 (for Lock Task Mode) and targetSdk 34+.
Dependencies: Open build.gradle.kts (Module: app) and ensure you have the standard Material and AppCompat libraries.
Phase 2: Manifest & Configuration
1. res/xml/device_admin_rules.xml
Create this folder and file. It allows the app to request "Lock Task" privileges.
code
Xml
<?xml version="1.0" encoding="utf-8"?>
<device-admin>
<uses-policies>
<lock-task />
</uses-policies>
</device-admin>
2. AndroidManifest.xml
This adds permissions, blocks screen recording, and enables the .seb file opening feature.
code
Xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="Secure Exam Browser">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="userLandscape"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Handle opening .seb files -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:scheme="file" />
<data android:host="*" />
<data android:pathPattern=".*\\.seb" />
</intent-filter>
</activity>
<receiver
android:name=".MyDeviceAdminReceiver"
android:label="SEB Admin"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin_rules" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
</application>
</manifest>
Phase 3: The Native Code
1. MyDeviceAdminReceiver.kt
Required to handle the administrative handshake.
code
Kotlin
package com.your.package.name
import android.app.admin.DeviceAdminReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
class MyDeviceAdminReceiver : DeviceAdminReceiver() {
override fun onEnabled(context: Context, intent: Intent) {
Toast.makeText(context, "SEB Protection Enabled", Toast.LENGTH_SHORT).show()
}
}
2. MainActivity.kt (The Core Engine)
This code handles the WebView, the lockdown, and the password-protected exit.
code
Kotlin
package com.your.package.name
import android.app.AlertDialog
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import android.text.InputType
import android.view.WindowManager
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private var quitPassword = "exit" // Default password
private lateinit var dpm: DevicePolicyManager
private lateinit var adminComponent: ComponentName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. SECURITY: Prevent Screenshots and Screen Recording
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
setContentView(R.layout.activity_main)
// 2. INITIALIZE LOCKDOWN UTILS
dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
adminComponent = ComponentName(this, MyDeviceAdminReceiver::class.java)
// 3. SETUP WEBVIEW
webView = findViewById(R.id.webview)
setupWebView()
// 4. ENTER LOCK TASK MODE (Lockdown)
enterLockdown()
// 5. HANDLE INCOMING CONFIG OR URL
val startUrl = intent.dataString ?: "https://www.google.com" // Default fallback
webView.loadUrl(startUrl)
}
private fun setupWebView() {
val settings = webView.settings
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
// Identify as SEB for LMS compatibility (Moodle/Canvas)
settings.userAgentString = "SEB_Android_Lockdown_Browser"
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
// Keep navigation inside the WebView
return false
}
}
// Disable Context Menus (Prevents Copy/Paste/Search)
webView.setOnLongClickListener { true }
}
private fun enterLockdown() {
try {
if (dpm.isDeviceOwnerApp(packageName)) {
// If app is Device Owner (via ADB), full silent lockdown
dpm.setLockTaskPackages(adminComponent, arrayOf(packageName))
}
// Start pinning/lockdown
startLockTask()
} catch (e: Exception) {
Toast.makeText(this, "Lockdown failed: ${e.message}", Toast.LENGTH_LONG).show()
}
}
// BLOCK THE BACK BUTTON
override fun onBackPressed() {
showQuitDialog()
}
private fun showQuitDialog() {
val builder = AlertDialog.Builder(this)
bui
0 Comments