A small, coroutine-first Kotlin Multiplatform library for storing secrets on
Android (EncryptedSharedPreferences over the Android Keystore) and
iOS (Keychain Services). One API, two native backends, no hand-rolled
cryptography.
val vault = SecureVault("com.acme.auth")
vault.put("session", "eyJhbGciOiJIUzI1NiJ9…")
val token: String? = vault.get("session")secure-vault is published to Maven Central. The Compose Multiplatform
integration ships separately as secure-vault-compose.
// build.gradle.kts
dependencies {
implementation("io.github.alims-repo:secure-vault:0.3.0")
// Optional — only if you use Compose Multiplatform:
implementation("io.github.alims-repo:secure-vault-compose:0.3.0")
}KMP source set wiring:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.alims-repo:secure-vault:0.3.0")
implementation("io.github.alims-repo:secure-vault-compose:0.3.0")
}
}
}| Target | Backend |
|---|---|
android |
EncryptedSharedPreferences |
iosArm64 |
Keychain Services |
iosSimulatorArm64 |
Keychain Services |
Both artifacts (secure-vault and secure-vault-compose) ship the same
target matrix. Common-side consumers depend on the artefact name; the matching
platform variant is selected by Gradle metadata automatically.
class AuthRepository(private val vault: SecureVault) {
suspend fun saveSession(token: String) = vault.put("session", token)
suspend fun session(): String? = vault.get("session")
suspend fun logout() = vault.remove("session")
/** Hot flow: re-emits whenever the session is written, removed, or cleared. */
fun sessionFlow(): Flow<String?> = vault.observe("session")
}No Context plumbing — the library captures the application Context via
androidx.startup
before Application.onCreate() returns.
class App : Application() {
override fun onCreate() {
super.onCreate()
// Anywhere from here on:
val vault = SecureVault("com.acme.auth")
}
}If you have explicitly disabled androidx.startup for SecureVault's
initializer, supply the context manually:
SecureVault.initialize(applicationContext)let vault = SecureVaultsKt.SecureVault(
namespace: "com.acme.auth",
accessibility: .afterFirstUnlock
)Every storage method throws a single sealed type — VaultException:
try {
vault.put("token", value)
} catch (e: VaultException.InvalidKey) { /* programmer error */ }
catch (e: VaultException.Tampered) { /* nuke the namespace */ }
catch (e: VaultException.CryptoFailure) { /* report */ }
catch (e: VaultException.StorageUnavailable) { /* retry / surface */ }Add the optional secure-vault-compose dependency for a small, opinionated
Compose layer: a lifecycle-aware rememberSecureVault(...) factory, a
VaultState sealed type so you render Initializing/Ready/Failed
explicitly, and a LocalSecureVault CompositionLocal.
@Composable
fun AppRoot() {
val state by rememberSecureVault("com.acme.auth")
when (val s = state) {
VaultState.Initializing -> SplashScreen()
is VaultState.Failed -> ErrorScreen(s.reason)
is VaultState.Ready -> ProvideSecureVault(s.vault) { HomeScreen() }
}
}
@Composable
fun HomeScreen() {
val vault = LocalSecureVault.current // available everywhere below ProvideSecureVault
// ... vault.put(...), vault.get(...)
}The underlying host is process-wide and keyed by VaultConfig.namespace, so
calling rememberSecureVault from multiple composables with the same namespace
returns the same vault and the master-key pre-warm runs exactly once.
Inside a ProvideSecureVault subtree, any composable can bind a single key
to a MutableState with rememberSecureValue. Reads track the vault via
observe(key); writes propagate back through put(key, value).
@Composable
fun LoginScreen() {
var token by rememberSecureValue("auth.token", default = "")
OutlinedTextField(value = token, onValueChange = { token = it })
}All suspending methods dispatch onto an I/O-appropriate dispatcher
(Dispatchers.IO on Android, Dispatchers.Default on iOS); callers do not
need to switch context. Instances are safe to share across coroutines.
See SECURITY.md for guarantees, non-goals, and the responsible-disclosure policy.
./gradlew :secure-vault:build
./gradlew :secure-vault:apiDump # after intentional API changes
./gradlew :secure-vault:publishToMavenLocalPublication requires ~/.gradle/gradle.properties to declare
mavenCentralUsername, mavenCentralPassword, and the in-memory
signingInMemoryKey / signingInMemoryKeyPassword properties consumed by
the vanniktech maven-publish
plugin.
Copyright 2026 Alim Sourav
Licensed under the Apache License, Version 2.0.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0