Skip to content

Commit bf7275f

Browse files
committed
Change Tag to version 1.2.0
1 parent 8ea1769 commit bf7275f

File tree

3 files changed

+163
-23
lines changed

3 files changed

+163
-23
lines changed

README.md

Lines changed: 161 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,17 @@ Whether you must squirrel away OAuth tokens in a fintech app or remember the las
1818
##### Contributors
1919
Special thanks to [Mark Andrachek](https://github.com/mandrachek) for his contribution!
2020

21-
2221
***
2322

24-
2523
## Why use KSafe?
2624

27-
* **Hardware-backed security** 🔐 AES‑256‑GCM with keys stored in Android Keystore or iOS Keychain for maximum protection.
25+
* **Hardware-backed security** 🔐 AES‑256‑GCM with keys stored in Android Keystore, iOS Keychain, or software-backed on JVM for maximum protection.
2826
* **Clean reinstalls** 🧹 Automatic cleanup ensures fresh starts after app reinstallation on both platforms.
2927
* **One code path** No expect/actual juggling—your common code owns the vault.
3028
* **Ease of use** `var launchCount by ksafe(0)` —that is literally it.
31-
* **Versatility** Primitives, data classes, sealed hierarchies, lists, sets; all accepted.
32-
* **Performance** Suspend API keeps the UI thread free; direct API is there when you need blocking simplicity.
29+
* **Versatility** Primitives, data classes, sealed hierarchies, lists, sets, and nullable types—all accepted.
30+
* **Performance** Zero-latency UI reads with the new Hybrid Cache architecture; suspend API keeps the UI thread free.
31+
* **Desktop Support** Full JVM/Desktop support alongside Android and iOS.
3332

3433
## How encryption works under the hood
3534

@@ -42,14 +41,19 @@ KSafe provides enterprise-grade encrypted persistence using DataStore Preference
4241
* **Access Control:** Keys only accessible when device is unlocked
4342

4443
##### iOS
45-
* **Cipher:** AES‑256‑GCM via OpenSSL-3 provider
44+
* **Cipher:** AES‑256‑GCM via CryptoKit provider
4645
* **Key Storage:** iOS Keychain Services
4746
* **Security:** Protected by device passcode/biometrics, not included in backups
4847
* **Access Control**: `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`
4948
* **Reinstall Handling:** Automatic cleanup of orphaned Keychain entries on first use
5049

51-
##### Flow
50+
##### JVM/Desktop
51+
* **Cipher:** AES-256-GCM via javax.crypto
52+
* **Key Storage:** Software-backed keys stored alongside data
53+
* **Security:** Relies on OS file permissions (0700 on POSIX systems)
54+
* **Location:** `~/.eu_anifantakis_ksafe/` directory
5255

56+
##### Flow
5357
* **Serialize value → plaintext bytes** using kotlinx.serialization.
5458
* **Load (or generate) a random 256‑bit AES key** from Keystore/Keychain (unique per preference key)
5559
* **Encrypt with AES‑GCM** (nonce + auth‑tag included).
@@ -69,21 +73,13 @@ Add the KSafe dependency to your `build.gradle.kts` (or `build.gradle`) file.
6973

7074
#### 1 - Add the Dependency
7175

72-
If you want to use the latest stable version
73-
```kotlin
74-
// commonMain or Android-only build.gradle(.kts)
75-
implementation("eu.anifantakis:ksafe:1.1.1")
76-
implementation("eu.anifantakis:ksafe-compose:1.1.1") // ← Compose state (optional)
77-
```
78-
79-
If you want to use the latest pre-release (Release Candidate) version with Desktop support
8076
```kotlin
8177
// commonMain or Android-only build.gradle(.kts)
82-
implementation("eu.anifantakis:ksafe:1.2.0-RC1")
83-
implementation("eu.anifantakis:ksafe-compose:1.2.0-RC1") // ← Compose state (optional)
78+
implementation("eu.anifantakis:ksafe:1.2.0")
79+
implementation("eu.anifantakis:ksafe-compose:1.2.0") // ← Compose state (optional)
8480
```
8581

86-
> Skip `ksafe-compose` if your project doesnt use Jetpack Compose, or if you don't intend to use the library's `mutableStateOf` persistance option
82+
> Skip `ksafe-compose` if your project doesn't use Jetpack Compose, or if you don't intend to use the library's `mutableStateOf` persistance option
8783
8884
#### 2 - Apply the kotlinx‑serialization plugin
8985

@@ -92,7 +88,7 @@ If you want to use the library with data classes, you need to enable Serializati
9288
Add Serialization definition to your `plugins` section of your `libs.versions.toml`
9389
```toml
9490
[versions]
95-
kotlin = "2.2.10"
91+
kotlin = "2.2.21"
9692

9793
[plugins]
9894
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
@@ -125,6 +121,11 @@ actual val platformModule = module {
125121
actual val platformModule = module {
126122
single { KSafe() }
127123
}
124+
125+
// JVM/Desktop
126+
actual val platformModule = module {
127+
single { KSafe() }
128+
}
128129
```
129130

130131
And now you're ready to inject KSafe to your ViewModels :)
@@ -197,6 +198,36 @@ authInfo = authInfo.copy(accessToken = "newToken")
197198
> ⚠️ Seeing "Serializer for class X' is not found"?
198199
Add `@Serializable` and make sure you have added Serialization plugin to your app
199200

201+
#### Nullable Values
202+
203+
KSafe fully supports nullable types. You can store and retrieve `null` values correctly:
204+
205+
```Kotlin
206+
// Store null values
207+
val token: String? = null
208+
ksafe.put("auth_token", token, encrypted = true)
209+
210+
// Retrieve null values (returns null, not defaultValue)
211+
val retrieved: String? = ksafe.get("auth_token", "default", encrypted = true)
212+
// retrieved == null ✓
213+
214+
// Works with all APIs
215+
ksafe.putDirect("key", null as String?, encrypted = false)
216+
val value: String? = ksafe.getDirect("key", "default", encrypted = false)
217+
// value == null ✓
218+
219+
// Nullable fields in serializable classes
220+
@Serializable
221+
data class UserProfile(
222+
val id: Int,
223+
val nickname: String?, // Can be null
224+
val bio: String? // Can be null
225+
)
226+
227+
val profile = UserProfile(1, null, "Hello!")
228+
ksafe.put("profile", profile, encrypted = true)
229+
```
230+
200231
#### Suspend API (non‑blocking)
201232

202233
```Kotlin
@@ -218,6 +249,13 @@ var clicks by ksafe.mutableStateOf(0) // encrypted backing storage
218249
actionButton { clicks++ }
219250
```
220251

252+
#### Jetpack Compose ♥ KSafe (optional module)
253+
as already mentioned above, Recomposition‑proof and survives process death with zero boilerplate.
254+
```Kotlin
255+
var clicks by ksafe.mutableStateOf(0) // encrypted backing storage
256+
actionButton { clicks++ }
257+
```
258+
221259
#### Deleting data
222260
```Kotlin
223261
ksafe.delete("profile") // suspend (non‑blocking)
@@ -312,6 +350,78 @@ class CounterViewModel(ksafe: KSafe) : ViewModel() {
312350

313351
***
314352

353+
## Architecture: Hybrid "Hot Cache" 🚀
354+
355+
KSafe 1.2.0 introduces a completely rewritten core architecture focusing on zero-latency UI performance.
356+
357+
### How It Works
358+
359+
**Before (v1.1.x):** Every `getDirect()` call triggered a blocking disk read and decryption on the calling thread. This could cause frame drops in scrollable environments.
360+
361+
**Now (v1.2.0):** Data is preloaded asynchronously immediately upon initialization. `getDirect()` now performs an **Atomic Memory Lookup (O(1))**, returning instantly.
362+
363+
**Safety:** If data is accessed before the preload finishes (Cold Start), the library automatically falls back to a blocking read to ensure you never receive incorrect default values.
364+
365+
### Optimistic Updates
366+
367+
`putDirect()` now updates the in-memory cache **immediately**, allowing your UI to reflect changes instantly while the disk encryption and write happen safely in the background.
368+
369+
***
370+
371+
## Memory Security Policy 🔒
372+
373+
You can now choose the trade-off between maximum performance and maximum security regarding data resident in RAM.
374+
375+
```Kotlin
376+
val ksafe = KSafe(
377+
fileName = "secrets",
378+
memoryPolicy = KSafeMemoryPolicy.ENCRYPTED // (Default) or PLAIN_TEXT
379+
)
380+
```
381+
382+
### Policy Options
383+
384+
| Policy | Best For | Behavior | Performance |
385+
|--------|----------|----------|-------------|
386+
| `ENCRYPTED` (Default) | Tokens, passwords, sensitive data | Stores raw ciphertext in RAM. Decrypts on-demand every time you ask for data, then discards the plaintext immediately. | Slightly higher CPU per read |
387+
| `PLAIN_TEXT` | User settings, themes, preferences | Decrypts once on load, stores plain values in RAM. | Instant reads, zero CPU overhead per call |
388+
389+
Both policies encrypt data on disk. The difference is how data is handled in memory:
390+
- **ENCRYPTED:** Maximum security against memory dump attacks
391+
- **PLAIN_TEXT:** Maximum performance for frequently accessed data
392+
393+
### Lazy Loading
394+
395+
By default, KSafe eagerly preloads data on initialization. If you want to defer loading until first access:
396+
397+
```Kotlin
398+
val archive = KSafe(
399+
fileName = "archive",
400+
lazyLoad = true // Skip preload, load on first request
401+
)
402+
```
403+
404+
### Constructor Parameters
405+
406+
```Kotlin
407+
// Android
408+
KSafe(
409+
context: Context,
410+
fileName: String? = null, // Optional namespace
411+
lazyLoad: Boolean = false, // Eager (false) or lazy (true) loading
412+
memoryPolicy: KSafeMemoryPolicy = KSafeMemoryPolicy.ENCRYPTED
413+
)
414+
415+
// iOS / JVM
416+
KSafe(
417+
fileName: String? = null,
418+
lazyLoad: Boolean = false,
419+
memoryPolicy: KSafeMemoryPolicy = KSafeMemoryPolicy.ENCRYPTED
420+
)
421+
```
422+
423+
***
424+
315425
## Security Features
316426
### Platform-Specific Protection
317427

@@ -327,6 +437,11 @@ class CounterViewModel(ksafe: KSafe) : ViewModel() {
327437
* Not included in iCloud/iTunes backups
328438
* Automatic cleanup of orphaned keys on first app use after reinstall
329439

440+
#### JVM/Desktop
441+
* AES-256-GCM encryption via standard javax.crypto
442+
* Keys stored in user home directory with restricted permissions
443+
* Suitable for desktop applications and server-side use
444+
330445
### Error Handling
331446
If decryption fails (e.g., corrupted data or missing key), KSafe gracefully returns the default value, ensuring your app continues to function.
332447

@@ -351,7 +466,8 @@ On iOS, KSafe uses a smart detection system:
351466

352467
* **iOS:** Keychain access requires device to be unlocked
353468
* **Android:** Some devices may not have hardware-backed keystore
354-
* **Both:** Encrypted data is lost if encryption keys are deleted (by design for security)
469+
* **JVM:** No hardware security module; relies on file system permissions
470+
* **All Platforms:** Encrypted data is lost if encryption keys are deleted (by design for security)
355471

356472
***
357473

@@ -369,6 +485,9 @@ KSafe includes comprehensive tests for all platforms. Here are the Gradle comman
369485
# Run common tests only
370486
./gradlew :ksafe:commonTest
371487

488+
# Run JVM tests
489+
./gradlew :ksafe:jvmTest
490+
372491
# Run Android unit tests (Note: May fail in Robolectric due to KeyStore limitations)
373492
./gradlew :ksafe:testDebugUnitTest
374493

@@ -454,12 +573,33 @@ xcrun devicectl device process launch \
454573
The iOS test app demonstrates:
455574
- Creating a KSafe instance with a custom file name
456575
- Observing value changes through Flow simulation (via polling)
457-
- For production apps, consider using [SKIE](https://skie.touchlab.co/) or [KMP-NativeCoroutines](https://github.com/rickclephas/KMP-NativeCoroutines) for easier Flow consumption from iOS
576+
- For production apps, consider using [SKIE](https://skie.touchlab.co/) or [KMP-NativeCoroutines](https://github.com/rickclephas/KMP-NativeCoroutines) for easier Flow consumption from iOS
458577
- Using `putDirect` to immediately update values
459578
- Real-time UI updates responding to value changes
460579

461580
***
462581

582+
## Migration from v1.1.x
583+
584+
### Binary Compatibility
585+
The public API surface (`get`, `put`, `getDirect`, `putDirect`) remains backward compatible.
586+
587+
### Behavior Changes
588+
- **Initialization is now eager by default.** If you relied on KSafe doing absolutely nothing until the first call, pass `lazyLoad = true`.
589+
- **Nullable values now work correctly.** No code changes needed, but you can now safely store `null` values.
590+
591+
### Compose Module Import Fix
592+
If upgrading from early 1.2.0 alphas, update your imports:
593+
```kotlin
594+
// Old (broken in alpha versions)
595+
import eu.eu.anifantakis.lib.ksafe.compose.mutableStateOf
596+
597+
// New (correct)
598+
import eu.anifantakis.lib.ksafe.compose.mutableStateOf
599+
```
600+
601+
***
602+
463603
## Licence
464604

465605
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.

ksafe-compose/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugins {
1111

1212
// Set the same group and version as your main library
1313
group = "eu.anifantakis"
14-
version = "1.2.0-RC1"
14+
version = "1.2.0"
1515

1616
kotlin {
1717
androidLibrary {

ksafe/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010
}
1111

1212
group = "eu.anifantakis"
13-
version = "1.2.0-RC1"
13+
version = "1.2.0"
1414

1515
kotlin {
1616
androidTarget {

0 commit comments

Comments
 (0)