Skip to content

ioannisa/KSafeDemo

Repository files navigation

KSafe Demo

A comprehensive Kotlin Multiplatform demo application showcasing KSafe - a secure encrypted storage library with biometric authentication, runtime security detection, and device lock-state protection.

Platforms: Android, iOS, Desktop (JVM), Browser (WASM/JS)


Screenshots

Storage Screen Custom JSON Screen Security Screen
image (screenshot pending) image

What This Demo Shows

This demo application serves as a practical guide to understanding and implementing KSafe in your own projects. It demonstrates:

1. Encrypted Persistent Storage

  • Compose State Integration: Using ksafe.mutableStateOf() for reactive, encrypted persistence
  • Property Delegation: Using by ksafe() for non-Compose encrypted properties
  • Encryption Options: Both encrypted (default) and unencrypted storage modes
  • Complex Data Types: Storing @Serializable data classes with automatic serialization

2. Biometric Authentication

  • Cross-Platform Support: Face ID, Touch ID, and Fingerprint authentication
  • Authorization Duration: Caching authentication for a specified time period with visible countdown
  • Scoped Authentication: Binding auth validity to ViewModel lifecycle

3. Security Policy (New in 1.4.0)

  • Runtime Security Detection: Root/Jailbreak, Debugger, Debug Build, Emulator
  • Configurable Actions: IGNORE, WARN, or BLOCK for each security check
  • Security Callbacks: Custom handling when violations are detected

4. Custom JSON Serialization (New in 1.7.1)

  • @Contextual Types: Storing data classes with third-party types you don't own (e.g., UUID, Instant)
  • Custom SerializersModule: Registering custom serializers once at the KSafe instance level
  • Both Modes: Works with encrypted and plain-text storage
  • Code Snippets: The screen itself displays the setup code for reference

5. WASM/JS Browser Support (New in 1.6.0)

  • Browser localStorage: Encrypted key-value storage in the browser via WebCrypto AES-256-GCM
  • Async Cache Initialization: awaitCacheReady() gates rendering until WebCrypto decryption completes
  • Same API: All KSafe features (property delegation, Compose state, StateFlow) work identically in the browser
  • Compose for Web: Full mutableStateOf persistence via ksafe-compose WASM target

6. Device Lock-State Protection (New in 1.5.0)

  • requireUnlockedDevice: Encrypted data is only accessible when the device is unlocked
  • Interactive Lock Test: 15-second countdown to lock your device, then verifies encrypted reads are blocked
  • Platform Background Tasks: iOS uses beginBackgroundTaskWithExpirationHandler to keep the test running while the screen is off

App Screens

Storage Screen

Demonstrates various ways to persist data with KSafe:

Feature Description
Counter 1 Regular Compose mutableStateOf - no persistence (resets on restart)
Counter 2 ksafe.mutableStateOf - encrypted persistent state
Counter 3 ksafe.mutableStateOf with encrypted = false - unencrypted persistent state
AuthInfo @Serializable data class with encrypted persistence
Biometric Count Counter protected by biometric authentication with authorization duration countdown
Lock Test Interactive test to verify requireUnlockedDevice blocks access when the device is locked

Custom JSON Screen

Demonstrates storing data classes that contain @Contextual types — types you don't own and can't annotate with @Serializable:

Feature Description
Two custom types Timestamp (Long) and HexColor (String) — stand-ins for types like Instant and Color
Two @Contextual fields UserProfile with @Contextual val createdAt and @Contextual val favoriteColor
Encrypted + Plain Same data stored in both modes to show it works everywhere
Step-by-step code The screen itself displays the 4-step setup as inline code snippets

Security Screen

Displays real-time security status of the device:

Check Description
Root/Jailbreak Detects rooted Android devices or jailbroken iOS devices
Debugger Detects if a debugger is attached to the process
Debug Build Detects if the app is running in debug mode
Emulator Detects if running on emulator/simulator

Code Examples from the Demo

Basic Encrypted State (LibCounterViewModel.kt)

class LibCounterViewModel(val ksafe: KSafe) : ViewModel() {

    // Regular Compose state - no persistence
    var count1 by mutableStateOf(1000)
        private set

    // KSafe encrypted state - persists across app restarts
    var count2 by ksafe.mutableStateOf(2000)
        private set

    // KSafe unencrypted state with custom key
    var count3 by ksafe.mutableStateOf(
        defaultValue = 3000,
        key = "counter3Key",
        encrypted = false
    )
        private set
}

Property Delegation (Non-Compose)

// Encrypted by default
var count4 by ksafe(10)
var count5 by ksafe(20)

// Encrypted string
var count6 by ksafe("30")

// Unencrypted string
var count7 by ksafe("40", encrypted = false)

Serializable Data Classes

@Serializable
data class AuthInfo(
    val accessToken: String = "",
    val refreshToken: String = "",
    val expiresIn: Long = 0L
)

var authInfo by ksafe.mutableStateOf(
    defaultValue = AuthInfo(
        accessToken = "abc",
        refreshToken = "def",
        expiresIn = 3600L
    ),
    key = "authInfo",
    encrypted = true
)

Biometric Authentication with Duration Cache

private val bioAuthDurationSeconds = 6

fun bioCounterIncrement() {
    ksafe.verifyBiometricDirect(
        reason = "Authenticate to save",
        authorizationDuration = BiometricAuthorizationDuration(
            duration = bioAuthDurationSeconds * 1000L,
            scope = viewModelScope.hashCode().toString()
        )
    ) { success ->
        if (success) {
            bioCount++
            // Start visible countdown only on fresh auth
            if (bioAuthRemaining == 0) {
                startBioAuthTimer()
            }
        }
    }
}

Custom JSON Serialization (CustomJsonViewModel.kt)

// 1. Define custom serializers for types you don't own
object TimestampSerializer : KSerializer<Timestamp> {
    override val descriptor = PrimitiveSerialDescriptor("Timestamp", PrimitiveKind.LONG)
    override fun serialize(encoder: Encoder, value: Timestamp) = encoder.encodeLong(value.epochMillis)
    override fun deserialize(decoder: Decoder) = Timestamp(decoder.decodeLong())
}

object HexColorSerializer : KSerializer<HexColor> {
    override val descriptor = PrimitiveSerialDescriptor("HexColor", PrimitiveKind.STRING)
    override fun serialize(encoder: Encoder, value: HexColor) = encoder.encodeString(value.hex)
    override fun deserialize(decoder: Decoder) = HexColor(decoder.decodeString())
}

// 2. Register all serializers in one place
val customJson = Json {
    ignoreUnknownKeys = true
    serializersModule = SerializersModule {
        contextual(TimestampSerializer)
        contextual(HexColorSerializer)
        // add as many as you need
    }
}

// 3. Pass it via KSafeConfig — one setup, used everywhere
val ksafe = KSafe(
    config = KSafeConfig(json = customJson)
)

// 4. Use @Contextual types directly — no extra work at the call site
@Serializable
data class UserProfile(
    val name: String,
    @Contextual val createdAt: Timestamp,
    @Contextual val favoriteColor: HexColor
)

var profile by ksafe.mutableStateOf(
    defaultValue = defaultProfile,
    key = "custom_json_profile",
    mode = KSafeWriteMode.Encrypted()
)

Note: kotlinx-serialization-json is provided as a transitive dependency by KSafe — no need to add it manually.

Device Lock-State Protection (Modules.android.kt)

actual val platformModule: Module
    get() = module {
        single<KSafe> {
            KSafe(
                context = androidApplication(),
                config = KSafeConfig(requireUnlockedDevice = true),
                securityPolicy = KSafeSecurityPolicy.Strict.copy(
                    debuggerAttached = SecurityAction.WARN,
                    debugBuild = SecurityAction.WARN,
                    emulator = SecurityAction.WARN,
                    onViolation = { violation ->
                        SecurityViolationsHolder.addViolation(violation)
                    }
                )
            )
        }
    }

WASM/JS Platform Setup (Modules.wasmJs.kt)

actual val platformModule: Module
    get() = module {
        single<KSafe> {
            KSafe(
                fileName = "wasmdata",
                securityPolicy = KSafeSecurityPolicy.WarnOnly.copy(
                    onViolation = { violation ->
                        SecurityViolationsHolder.addViolation(violation)
                    }
                )
            )
        }
    }

WASM Entry Point (main.kt)

fun main() {
    val body = document.body ?: return
    ComposeViewport(body) {
        KoinMultiplatformApplication(config = createKoinConfiguration()) {
            var cacheReady by remember { mutableStateOf(false) }

            LaunchedEffect(Unit) {
                val ksafe: KSafe = getKoin().get()
                ksafe.awaitCacheReady()
                cacheReady = true
            }

            if (cacheReady) {
                AppContent()
            }
        }
    }
}

Note: On WASM, Koin must be initialized before awaitCacheReady() can retrieve the KSafe instance. The AppContent() composable (extracted from App()) renders only after the async WebCrypto initialization completes.

Flow-based Reactive Updates

viewModelScope.launch {
    ksafe.getFlow<String?>(
        key = "access-token",
        defaultValue = null,
        encrypted = true
    ).collect { value ->
        println("Token changed: $value")
    }
}

Security Policies

KSafe 1.4.0 introduces configurable security policies:

Policy Root/Jailbreak Debugger Debug Build Emulator
Default IGNORE IGNORE IGNORE IGNORE
WarnOnly WARN WARN WARN WARN
Strict BLOCK BLOCK WARN WARN

Security Actions

Action Behavior
IGNORE No detection performed
WARN Detection runs, callback invoked, app continues
BLOCK Detection runs, callback invoked, throws SecurityViolationException

Lock-State Policy Test

The demo includes an interactive test for the requireUnlockedDevice feature:

  1. Tap "Test Lock Feature" - a test value is pre-stored while the device is unlocked
  2. A 15-second countdown begins - lock your device during this time
  3. After the countdown, the app attempts to read the encrypted value from the Keychain/Keystore
  4. Results:
    • "READ BLOCKED" - The feature works correctly; the Keychain/Keystore denied access while locked
    • "READ SUCCEEDED" - If running from Xcode with the debugger, this is expected (see below)

Important (iOS): Xcode's debugger prevents iOS data protection from fully engaging. For accurate results:

  1. Build & run the app on your device from Xcode
  2. Press Stop in Xcode to disconnect the debugger
  3. Launch the app from the Home Screen
  4. Run the lock test

Biometric Authentication Architecture

This demo implements cross-platform biometric authentication using the expect/actual pattern:

commonMain/
└── biometric/
    └── BiometricAuthenticator.kt      # Interface + expect function

androidMain/
└── biometric/
    └── BiometricAuthenticator.android.kt  # BiometricPrompt

iosMain/
└── biometric/
    └── BiometricAuthenticator.ios.kt      # LocalAuthentication (Face ID/Touch ID)

jvmMain/
└── biometric/
    └── BiometricAuthenticator.jvm.kt      # Auto-authenticate (no hardware)

Interface

interface BiometricAuthenticator {
    fun isAvailable(): Boolean
    fun authenticate(
        title: String,
        subtitle: String,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    )
}

@Composable
expect fun rememberBiometricAuthenticator(): BiometricAuthenticator

Project Structure

composeApp/src/
├── commonMain/kotlin/eu/anifantakis/ksafe_demo/
│   ├── App.kt                           # Navigation with bottom tabs
│   ├── biometric/
│   │   └── BiometricAuthenticator.kt    # expect interface
│   ├── di/
│   │   ├── Modules.kt                   # Shared DI module
│   │   └── SecurityViolationsHolder.kt  # Security violations state
│   ├── util/
│   │   └── BackgroundTask.kt            # expect for platform background tasks
│   └── screens/
│       ├── counters/
│       │   ├── LibCounterScreen.kt      # Storage demo UI
│       │   └── LibCounterViewModel.kt   # Storage demo logic
│       ├── customjson/
│       │   ├── CustomJsonScreen.kt      # Custom JSON demo UI
│       │   └── CustomJsonViewModel.kt   # @Contextual types + custom SerializersModule
│       └── security/
│           ├── SecurityScreen.kt        # Security status UI
│           └── SecurityViewModel.kt     # Security status logic
│
├── androidMain/kotlin/eu/anifantakis/ksafe_demo/
│   ├── MainActivity.kt
│   ├── biometric/BiometricAuthenticator.android.kt
│   ├── util/BackgroundTask.android.kt    # No-op (Android doesn't suspend)
│   └── di/Modules.android.kt            # KSafe with requireUnlockedDevice
│
├── iosMain/kotlin/eu/anifantakis/ksafe_demo/
│   ├── MainViewController.kt
│   ├── biometric/BiometricAuthenticator.ios.kt
│   ├── util/BackgroundTask.ios.kt        # beginBackgroundTask for lock test
│   └── di/Modules.ios.kt
│
├── jvmMain/kotlin/eu/anifantakis/ksafe_demo/
│   ├── main.kt
│   ├── biometric/BiometricAuthenticator.jvm.kt
│   ├── util/BackgroundTask.jvm.kt        # No-op
│   └── di/Modules.jvm.kt
│
└── wasmJsMain/kotlin/eu/anifantakis/ksafe_demo/
    ├── main.kt                            # ComposeViewport + awaitCacheReady
    ├── util/BackgroundTask.wasmJs.kt      # No-op
    └── di/Modules.wasmJs.kt              # KSafe with localStorage + WebCrypto

Build & Run

Android

./gradlew :composeApp:assembleDebug
./gradlew :composeApp:installDebug

Desktop (JVM)

./gradlew :composeApp:run

iOS

Open iosApp/iosApp.xcodeproj in Xcode and run.

Browser (WASM/JS)

./gradlew :composeApp:wasmJsBrowserDevelopmentRun

Then open http://localhost:8080/ in your browser.


Dependencies

// build.gradle.kts
commonMain.dependencies {
    implementation("eu.anifantakis:ksafe:1.7.1")
    implementation("eu.anifantakis:ksafe-compose:1.7.1")
}

kotlinx-serialization-json and biometric support (androidx.biometric) are included transitively through KSafe — no explicit dependencies needed.


Key Takeaways

  1. Seamless Encryption: KSafe makes encrypted storage as simple as regular storage
  2. Compose Integration: mutableStateOf works exactly like Compose's native state
  3. Cross-Platform: Same API across Android, iOS, Desktop, and Browser
  4. Security-First: Runtime security detection helps protect sensitive data
  5. Biometric Ready: Built-in support for biometric authentication with duration caching
  6. Lock-State Protection: requireUnlockedDevice ensures encrypted data is inaccessible when the device is locked
  7. Custom JSON: Support for @Contextual types via custom SerializersModule — store any type

Resources


License

This demo is provided as-is for educational purposes. See the KSafe library for licensing information.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors