KSafe: Secure Kotlin Multiplatform Key/Value Persistence

repository·main·Indexed 43 days ago

https://github.com/ioannisa/ksafe

A secure-by-default Kotlin Multiplatform (KMP) library for persisting variables, Compose state, and serializable objects across Android, iOS, macOS, Desktop, and Web. KSafe features AES-256-GCM encryption by default, support for Secure Enclave and StrongBox hardware isolation, and a synchronous API with a hot in-memory cache. It provides property delegates, reactive Flow support, and specialized integration for Jetpack Compose via the ksafe-compose module.

Tokens
89.5K
Snippets
188
Records
304
Agent score
56%

What's inside KSafe

  1. What is KSafe?

    main

    KSafe is a secure-by-default Kotlin Multiplatform (KMP) key/value persistence library. It allows you to persist ordinary Kotlin variables, Compose MutableState, MutableStateFlow, and @Serializable objects across app restarts on Android, iOS, macOS, JVM/Desktop, WASM, and Kotlin/JS.

    Key Features

    • Encrypted by default: Uses AES-256-GCM. You can opt-out to plain storage using mode = KSafeWriteMode.Plain.
    • Synchronous API: Reads hit a hot in-memory cache, making them non-blocking. Writes are performed in the background.
    • Asynchronous API: Provides a suspend API (get / put) if you need to explicitly await a disk flush.
    • Property Delegate: Use the ksafe() delegate to treat persisted values like normal variables without boilerplate.
    var counter by ksafe(0)
    counter++   // auto-encrypted (AES-256-GCM), auto-persisted, survives process death
  2. Understand the KSafe source layout and module structure

    main

    KSafe is organized into several source sets to support Kotlin Multiplatform (KMP) targets. Understanding this layout helps identify where platform-specific behavior is implemented and where the public API resides.

    Core Modules

    • commonMain: Contains the public API (e.g., KSafe.kt, KSafeConfig.kt) and the shared orchestrator (KSafeCore). This is the primary entry point for consumers.
    • datastoreMain: Shared logic for Android, Apple, and JVM targets, specifically handling the DataStore-based storage and commit relay.
    • jvmSharedMain: Shared logic for JVM-bytecode targets (Android and JVM), including crypto and concurrency implementations.
    • androidMain: Android-specific implementations, including AndroidKeystoreEncryption and AndroidLockScreen integration.
    • appleMain: Shared logic for iOS and macOS, utilizing a CryptoKit bridge via Swift.
    • jvmMain: JVM-specific factory functions and keyvault abstractions (Windows DPAPI, macOS Keychain, Linux Secret Service).
    • webMain: Shared logic for JS and WASM targets, utilizing SubtleCrypto, IndexedDB, and localStorage.

    Key Public API Components (in commonMain)

    • KSafe: The main entry point.
    • KSafeConfig: Configuration settings.
    • KSafeProtectionLevel: A universally-ordered scale for security levels.
    • KSafeModeViews: Provides views like KSafePlain, KSafeEncrypted, and KSafeHardwareIsolated (3.1.0+).
    • KSafeReference: The handle returned by ksafe(default, key) (3.2.0+).
  3. Explore KSafe documentation topics

    main

    The KSafe documentation is organized into several specialized guides. For new users, the recommended starting points are the USAGE and SETUP guides.

    Key documentation areas include:

    • API & Usage: Delegates, flows, Compose state, write modes, and ViewModel integration.
    • Setup & Configuration: Koin integration, multi-instance, web awaitCacheReady(), and appNamespace.
    • Security & Encryption: Biometric authentication, security models (root/debugger detection), key rotation, and JVM-specific key protection (DPAPI, Keychain, etc.).
    • Advanced Features: Custom JSON serialization, memory exposure policies, and performance benchmarks.
    • Architecture & Comparison: Conceptual models (modules, rings, hot cache) and comparisons against alternatives like DataStore or SQLCipher.
  4. Compare KSafe with other persistence libraries

    main

    KSafe is a Kotlin Multiplatform (KMP) library designed for universal key/value persistence with a focus on security, zero boilerplate, and developer ergonomics. Use the following comparison to decide if KSafe fits your needs:

    FeatureKSafeEncryptedSharedPrefsKVaultMultiplatform SettingsSQLCipher
    KMP Support✅ Android, iOS, macOS, JVM Desktop, WASM, JS❌ Android only✅ Android, iOS✅ Multi-platform⚠️ Limited
    Hardware-backed Keys✅ Android Keystore, Apple Keychain / Secure Enclave✅ Keystore✅ Keystore/Keychain❌ No encryption❌ Software
    Zero Boilerplate✅ Property delegates❌ Verbose API⚠️ Moderate⚠️ Moderate❌ SQL required
    Key Rotation✅ On demand or via MaxAge policy❌ Manual❌ Manual❌ Manual⚠️ Manual re-key
    Biometric Helper✅ Integrated via :ksafe-biometrics❌ Manual❌ Manual❌ Manual❌ Manual
    Compose State✅ Integrated via :ksafe-compose❌ Manual❌ Manual❌ Manual❌ Manual
    Type Safety✅ Reified generics⚠️ Limited✅ Good✅ Good❌ SQL strings
    Auth Caching✅ Per-duration, per-scope❌ No❌ No❌ No❌ No
    Operational Preflight✅ ksafe.protectionInfo.isEncryptionOperational❌ No❌ No❌ No❌ No
  5. Rules for multiple KSafe instances

    main

    When working with multiple storage files or instances, follow these constraints:

    1. Use Singletons: Each KSafe(fileName=...) should be a singleton created via DI. Creating multiple instances for the same file is wasteful and can cause issues.
    2. Web Warning: Multiple instances targeting the same file will have diverging caches on the Web. Use a single instance.
    3. Single Process Only: KSafe is designed for a single process. Do not access the same fileName from a second process (e.g., a background service or widget). Use a unique fileName for different processes.
    4. Naming Constraints: fileName must follow the pattern [a-z][a-z0-9_]* (starts with lowercase, contains only lowercase letters, digits, or underscores).
    5. Reserved Key Patterns: Avoid keys that start with __ksafe_ or encrypted_, or keys that end with KSafe internal sentinel segments (e.g., __ksafe_master__, __ksafe_strict__, or JVM vault markers). Writing these keys will throw an IllegalArgumentException.
  6. Understand the KEK / DEK security model

    main

    KSafe uses a distinction between a KEK (Key-Encryption Key) and a DEK (Data-Encryption Key) to balance security and performance across different platforms.

    • KEK (Key-Encryption Key): A key that stays in a secure hardware/OS store (like Android Keystore, Apple Keychain, or a JVM OS vault) and is used only to wrap (encrypt) the DEK.
    • DEK (Data-Encryption Key): The actual AES key used to encrypt your data. It is stored wrapped (encrypted) at rest and is unwrapped into process memory (RAM) to perform fast userspace AES operations.

    Platform Implementation Summary

    PlatformKEK CustodyDEK RoleAES Execution
    Android (DEFAULT)Keystore (TEE)Held in RAMUserspace (javax.crypto)
    Android (HARDWARE_ISOLATED)StrongBox / TEENone (Key never leaves hardware)Inside TEE / StrongBox
    Apple (DEFAULT)KeychainCached in RAMUserspace (CryptoKit)
    Apple (HARDWARE_ISOLATED)Secure EnclaveUnwrapped into RAMUserspace (CryptoKit)
    JVM / DesktopOS Vault (DPAPI/Keychain)In RAMUserspace (JCE)
    Web (JS / WASM)Browser (IndexedDB)None (Raw bytes never exist)Browser (crypto.subtle)

    Security Note: On most platforms (Android-DEFAULT, Apple-DEFAULT, JVM), the working AES key (DEK) lives in process RAM after its first use. If you require that the key never enters app memory, you must use HARDWARE_ISOLATED on Android.

  7. Use Associated Authenticated Data (AAD) for encryption integrity

    main
    When calling encryption methods, you can provide aad (Associated Authenticated Data). These are bytes that are authenticated but not encrypted. For decryption to succeed, the exact same aad bytes must be provided; otherwise, the AES-GCM tag check will fail and the read will error out. This is used to bind the encrypted data to a specific context.
  8. Configure Authorization Duration Caching

    main

    You can prevent repeated prompts by caching a successful authentication for a specific duration using BiometricAuthorizationDuration.

    Configuration Options

    • duration: The time in milliseconds the authorization remains valid. If 0 or less, the prompt will always appear (no caching).
    • scope: A string used as a cache key.
      • If null (default), it uses a global slot. Any other call with scope = null will skip the prompt if the cache is valid.
      • If provided (e.g., "settings_screen"), only calls with the exact same scope string will benefit from the cache.

    Cache Rules

    • The cache is in-memory only and is cleared on process restart.
    • Each scope maintains two separate slots: one for permissive mode (allowDeviceCredentialFallback = true) and one for strict mode (allowDeviceCredentialFallback = false). A permissive success cannot satisfy a strict request.
    • Freshness is checked against the duration of the current call, not the one that filled the slot.
    data class BiometricAuthorizationDuration(
        val duration: Long,       // milliseconds the authorization stays valid
        val scope: String? = null // which slot it is stored in; null = the global slot
    )
    
    // Example: Remember success for 60 seconds for the settings screen
    KSafeBiometrics.verifyBiometricDirect(
        reason = "Authenticate",
        authorizationDuration = BiometricAuthorizationDuration(
            duration = 60_000L,
            scope = "settings_screen"
        )
    ) { success -> /* ... */ }
  9. Platform behavior for KSafe Biometrics

    main

    KSafe Biometrics maps to native authentication APIs across platforms:

    PlatformImplementation
    AndroidBiometricPrompt (Requires AppCompatActivity; auto-initialized via ContentProvider)
    iOSLAContext (Face ID / Touch ID)
    macOSLAContext (Touch ID, password, or Apple Watch)
    JVM DesktopmacOS Touch ID / Windows Hello (on by default from 2.2.1)
    WebWebAuthn platform authenticator (on by default from 2.2.1)
    JVM LinuxNo portable prompt API; returns true to allow business logic to compile/run

    macOS Specifics: Behavior depends on the allowDeviceCredentialFallback flag:

    • true (default): Uses LAPolicyDeviceOwnerAuthentication (Touch ID + password + Apple Watch). Always prompts even without Touch ID.
    • false: Uses LAPolicyDeviceOwnerAuthenticationWithBiometrics (Touch ID only; returns false on hardware-less Macs).
  10. How key rotation works in KSafe

    main

    Key rotation allows KSafe to re-encrypt all stored data under fresh key material without changing the underlying values or requiring data migration. This is achieved by updating the Envelope (the on-disk shape containing ciphertext and metadata) while the plaintext remains untouched.

    Core Concepts

    • Key generation: A counter (starting at 1) that names the current set of encryption keys. Calling rotateKeys() increments this number and mints fresh keys. KSafe can hold multiple generations simultaneously, ensuring the store remains readable even if some entries haven't been rotated yet.
    • Envelope: The metadata record for an encrypted value that specifies which key and format were used. Rotation rewrites these envelopes.
    • Master key: The primary key used for DEFAULT entries. Every new generation mints its own master key. On Android and Apple, two master keys are minted (one for entries requiring an unlocked device and one for the rest). HARDWARE_ISOLATED entries use their own unique keys instead of a shared master key.

    When to rotate

    Rotation is not a security necessity because platform-level keys (Android Keystore, Apple Keychain, etc.) do not expire. However, it is a vital hygiene/compliance control for standards like PCI DSS or SOC 2, and a useful way to re-key everything after a suspected device compromise.

    val result = ksafe.rotateKeys() // suspend
  11. How KSafe manages the memory cache and cold starts

    main

    KSafe uses a multi-layered caching strategy to balance performance and security. The behavior of the cache depends heavily on the KSafeWriteMode used.

    Cache Types

    • memoryCache: The primary hot cache. Stores plaintext for PLAIN_TEXT mode, or Base64 ciphertext for ENCRYPTED modes.
    • plaintextCache: A secondary cache used for ENCRYPTED_WITH_TIMED_CACHE (TTL-bounded) and LAZY_PLAIN_TEXT (permanent) modes. It is filled lazily upon the first read of a key.

    Cold Start Behavior

    • ENCRYPTED / LAZY_PLAIN_TEXT: These modes support fast cold starts. Encrypted entries land in the cache as ciphertext, and decryption is deferred until the specific key is actually read. This makes cold start time independent of the total amount of ciphertext in the store.
    • PLAIN_TEXT: Requires a bulk-decrypt pass during the initial snapshot merge, which can impact startup time if the store is large.
  12. Understand the KSafe on-disk storage format

    main

    KSafe stores values using canonical raw keys in the underlying DataStore. Understanding this format is useful for debugging storage or implementing manual migrations.

    Key Mapping:

    • Value bytes: __ksafe_value_<userKey>
    • Metadata: __ksafe_meta_<userKey>__ (A JSON blob containing envelope version, protection level, access policy, and the generation key used for decryption).
    • Store key generation: __ksafe_keygen__ (A reserved entry managing the rotation lifecycle).

    Key Generation (__ksafe_keygen__) Schema:

    • g: The current key generation.
    • ts: Timestamp of when the generation was created (used by MaxAge policies).
    • r: Pass lifecycle state (0 for completed, 1 for in progress).
    • rp: (Optional) The remaining retry budget for automatic next-instance retries.