Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4

[*.{kt,kts}]
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *
ij_kotlin_packages_to_use_import_on_demand = unset
max_line_length = 120

[*.{yml,yaml,toml,md}]
indent_size = 2

[Makefile]
indent_style = tab

40 changes: 40 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Publish to Maven Central

# Triggers on any tag that starts with "v" — e.g. v0.5.7-beta, v1.0.0
on:
push:
tags:
- 'v*'

jobs:
publish:
name: Publish
runs-on: macos-latest # macOS is required to compile iOS KLIBs

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21.0.7'
distribution: 'temurin'
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Publish to Maven Central
# SIGNING_KEY = base64-encoded armored private key (avoids multiline env-var issues)
run: |
export ORG_GRADLE_PROJECT_signingInMemoryKey=$(echo "$SIGNING_KEY_B64" | base64 -d)
./gradlew :secure-vault:publishAllPublicationsToMavenCentralRepository --no-configuration-cache
Comment on lines +33 to +35
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_KEY_B64: ${{ secrets.SIGNING_KEY }}
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Changelog

All notable changes to **SecureVault KMP** are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.1.0] - 2026-05-26
### Added
- Initial public API:
- `SecureVault` interface — `put` / `get` / `remove` / `contains` /
`clear` / `keys`, all `suspend`.
- `SecureVaultFactory` `expect class` per platform.
- `VaultConfig` + `Accessibility` enum.
- `VaultException` sealed hierarchy
(`InvalidKey`, `CryptoFailure`, `Tampered`, `StorageUnavailable`).
- Android backend powered by
[`androidx.security:security-crypto`](https://developer.android.com/jetpack/androidx/releases/security)
(AES-256 SIV keys + AES-256 GCM values via Android Keystore).
- iOS backend powered by Keychain Services with `kSecClassGenericPassword`
and per-namespace `kSecAttrService` isolation.
- `commonTest` behavioural contract (`SecureVaultContractTest`) + in-memory
`FakeSecureVault`.
- Maven Central publishing pipeline (vanniktech 0.30, Dokka HTML javadoc jar,
signed) and binary-compatibility-validator-enforced public ABI.

### Known limitations
- Android Robolectric tests are deferred until AGP's
`com.android.kotlin.multiplatform.library` plugin exposes a stable
host-test surface.
- `Accessibility.WhenUnlocked` on Android currently maps to the same
EncryptedSharedPreferences scheme as `AfterFirstUnlock`; full user-presence
enforcement (biometric prompt) is tracked for v0.2.
- No JVM/Desktop target yet; planned for v0.2.

[Unreleased]: https://github.com/Alims-Repo/SecureVault-KMP/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/Alims-Repo/SecureVault-KMP/releases/tag/v0.1.0

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

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

http://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.

Copyright 2026 Alim Sourav

The full license text is available at:
https://www.apache.org/licenses/LICENSE-2.0.txt

153 changes: 136 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,143 @@
This is a Kotlin Multiplatform project targeting Android, iOS.
# SecureVault KMP

* [/iosApp](./iosApp/iosApp) contains an iOS application. Even if you’re sharing your UI with Compose Multiplatform,
you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
[![Maven Central](https://img.shields.io/maven-central/v/io.github.alimsrepo/secure-vault?style=flat-square)](https://central.sonatype.com/artifact/io.github.alimsrepo/secure-vault)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square)](LICENSE)
[![Kotlin](https://img.shields.io/badge/kotlin-2.3-blueviolet?style=flat-square&logo=kotlin)](https://kotlinlang.org)
[![Targets](https://img.shields.io/badge/targets-Android%20%7C%20iOS-success?style=flat-square)](#supported-targets)

* [/shared](./shared/src) is for code that will be shared across your Compose Multiplatform applications.
It contains several subfolders:
- [commonMain](./shared/src/commonMain/kotlin) is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name.
For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app,
the [iosMain](./shared/src/iosMain/kotlin) folder would be the right place for such calls.
Similarly, if you want to edit the Desktop (JVM) specific part, the [jvmMain](./shared/src/jvmMain/kotlin)
folder is the appropriate location.
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.

### Running the apps
```kotlin
val vault = SecureVaultFactory(context /* Android only */)
.create(VaultConfig(namespace = "com.acme.auth"))

Use the run configurations provided by the run widget in your IDE's toolbar. You can also use these commands and options:

- Android app: `./gradlew :androidApp:assembleDebug`
- iOS app: open the [/iosApp](./iosApp) directory in Xcode and run it from there.
vault.put("session", "eyJhbGciOiJIUzI1NiJ9…")
val token: String? = vault.get("session")
```

---

Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)…
## Install

`secure-vault` is published to Maven Central.

```kotlin
// build.gradle.kts
dependencies {
implementation("io.github.alimsrepo:secure-vault:0.1.0")
}
```

KMP source set wiring:

```kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.alimsrepo:secure-vault:0.1.0")
}
}
}
```

## Supported targets

| Target | Backend |
|----------------------|-----------------------------------|
| `android` | `EncryptedSharedPreferences` |
| `iosArm64` | Keychain Services |
| `iosSimulatorArm64` | Keychain Services |

Common-side consumers depend on `secure-vault`; the matching platform
artefact is selected by Gradle metadata automatically.

## Usage

### Common code

```kotlin
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")
}
```

### Android entry point

```kotlin
class App : Application() {
override fun onCreate() {
super.onCreate()
val vault = SecureVaultFactory(this)
.create(VaultConfig(namespace = "com.acme.auth"))
// pass `vault` to your DI graph
}
}
```

### iOS entry point (Swift)

```swift
let vault = SecureVaultFactory().create(
config: VaultConfig(
namespace: "com.acme.auth",
accessibility: .afterFirstUnlock
)
)
```

## Error handling

Every storage method throws a single sealed type — `VaultException`:

```kotlin
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 */ }
```

## Threading

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.

## Security model

See [SECURITY.md](SECURITY.md) for guarantees, non-goals, and the
responsible-disclosure policy.

## Building locally

```bash
./gradlew :secure-vault:build
./gradlew :secure-vault:apiDump # after intentional API changes
./gradlew :secure-vault:publishToMavenLocal
```

Publication requires `~/.gradle/gradle.properties` to declare
`mavenCentralUsername`, `mavenCentralPassword`, and the in-memory
`signingInMemoryKey` / `signingInMemoryKeyPassword` properties consumed by
the [vanniktech maven-publish](https://vanniktech.github.io/gradle-maven-publish-plugin/)
plugin.

## License

```
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
```

43 changes: 43 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Security Policy

## Supported versions

| Version | Supported |
|---------|-----------|
| 0.1.x | ✅ |

## Reporting a vulnerability

Please do **not** open a public GitHub issue for security reports.

Instead, email **sourav.0.alim@gmail.com** with:

- A description of the issue and its impact.
- Reproduction steps or a proof-of-concept.
- The affected version(s).

You should receive an acknowledgement within **72 hours**. A fix or
mitigation will be coordinated privately, followed by a patch release and
a public disclosure crediting the reporter (unless anonymity is requested).

## Threat model & non-goals

SecureVault wraps the platform's native secure storage:

- **Android** — `EncryptedSharedPreferences` backed by the Android Keystore.
- **iOS** — Keychain Services (`kSecClassGenericPassword`, `*ThisDeviceOnly`).

It therefore inherits the OS guarantees and limitations. In particular,
SecureVault does **not** protect against:

- A rooted/jailbroken device, or any attacker with code execution inside
the host process.
- Backups: stored values are excluded from device backups by the underlying
schemes, but users can still snapshot their device with developer tools.
- Memory inspection while a value is in flight (`String` is decrypted in
user space).

If your threat model requires hardware-backed user-presence enforcement,
set `Accessibility.WhenUnlocked` and (on Android, in a future release)
opt into biometric prompts.

1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ plugins {
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.androidLint) apply false
}
Loading
Loading