You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Add WASM/JS as fourth platform target with WebCrypto AES-256-GCM encryption and localStorage persistence.
- New reactive StateFlow API (getFlow/getStateFlow) for all platforms.
- Fix key generation race conditions on JVM, deleteKey cache coherence on Android/JVM, and DataStore multi-instance crash on Android DI re-initialization.
- Remove IntegrityChecker (unrelated Play Integrity/DeviceCheck wrapper).
Copy file name to clipboardExpand all lines: CHANGELOG.md
+93-24Lines changed: 93 additions & 24 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,99 @@
2
2
3
3
All notable changes to KSafe will be documented in this file.
4
4
5
+
## [1.6.0] - 2025-02-16
6
+
7
+
### Added
8
+
9
+
#### WASM/JS Target
10
+
11
+
KSafe now runs in the browser. New platform source sets (`wasmJsMain`, `wasmJsTest`) and a `ksafe-compose` WASM target bring encrypted key-value storage to Kotlin/WASM.
12
+
13
+
-**Storage:** Browser `localStorage` via `@JsFun` externals (in `LocalStorage.kt`)
14
+
-**Encryption:** WebCrypto AES-256-GCM via `cryptography-provider-webcrypto`
15
+
-**Memory policy:** Always `PLAIN_TEXT` internally — WebCrypto is async-only, so all values are decrypted at init and held as plaintext in a `HashMap`
16
+
-**Key namespace:**`ksafe_{fileName}_{key}` for data, `ksafe_key_{alias}` for encryption keys
17
+
-**Mutex-protected key generation** to prevent race conditions in single-threaded coroutine environments
18
+
-**Per-operation error isolation** in batch writes — a single failed operation doesn't discard the entire batch
19
+
-**`yield()`-based StateFlow propagation** — required because WASM is single-threaded and has no implicit suspension points like JVM's DataStore I/O
20
+
-**No**`runBlocking`, `ConcurrentHashMap`, `Dispatchers.IO`, or `AtomicBoolean` — all replaced with WASM-compatible equivalents
21
+
- New WASM-specific `actual` implementations: `KSafe.wasmJs.kt`, `WasmSoftwareEncryption.kt`, `LocalStorage.kt`, `SecurityChecker.wasmJs.kt`
22
+
23
+
#### StateFlow API
24
+
25
+
New reactive API for observing KSafe values as flows, available on all four platforms:
26
+
27
+
-`getFlow(key, defaultValue, encrypted)` — returns a cold `Flow<T>` that emits whenever the underlying value changes
28
+
-`getStateFlow(key, defaultValue, encrypted, scope)` — convenience extension that converts the cold flow into a hot `StateFlow<T>` using `stateIn(scope, SharingStarted.Eagerly, defaultValue)`
29
+
- Works with both encrypted and unencrypted values
30
+
- On DataStore platforms (Android, JVM, iOS), backed by `DataStore.data.map {}` with `distinctUntilChanged()`
31
+
- On WASM, backed by a `MutableStateFlow` that is updated on writes
32
+
33
+
#### ksafe-compose WASM Target
34
+
35
+
The `ksafe-compose` module now includes a `wasmJs` target, enabling `mutableStateOf` persistence in Compose for Web.
36
+
37
+
### Fixed
38
+
39
+
#### Android: DataStore "multiple active instances" crash on DI re-initialization
40
+
41
+
When using Koin Compose Multiplatform (`KoinMultiplatformApplication {}`), the Koin application context can be recreated on Activity restart (configuration changes such as rotation, locale change, or dark mode toggle). This causes all `single {}` definitions to be re-instantiated, including KSafe. Each new KSafe instance created a new DataStore for the same file, triggering:
42
+
43
+
```
44
+
IllegalStateException: There are multiple DataStores active for the same file
45
+
```
46
+
47
+
**Root cause:** Each `KSafe` constructor eagerly created its own `DataStore` instance. DataStore enforces a single-instance-per-file invariant. When Koin re-created KSafe with the same `fileName`, a second DataStore was created for the same file.
48
+
49
+
**Fix:** Added a process-level `ConcurrentHashMap<String, DataStore<Preferences>>` cache in the Android `companion object`. If a DataStore already exists for a given file name, it is reused instead of creating a new one. This fix is Android-only because configuration changes (Activity recreation) are an Android-specific lifecycle concept — iOS, JVM, and WASM do not re-initialize their DI containers during normal operation.
50
+
51
+
#### Key generation race condition (JVM)
52
+
53
+
Concurrent `putEncrypted` calls for the same key alias could trigger parallel key generation in `JvmSoftwareEncryption`, producing different AES keys. One key would be stored in DataStore while a different one was used to encrypt data, causing permanent data loss on the next read.
54
+
55
+
**Fix:** Added a `ConcurrentHashMap<String, SecretKey>` in-memory key cache and per-alias `synchronized` locks to `JvmSoftwareEncryption.getOrCreateSecretKey()`. The first caller generates and caches the key; subsequent callers return the cached key immediately.
56
+
57
+
#### deleteKey race with key cache repopulation (Android + JVM)
58
+
59
+
`deleteKey()` removed the key from the Keystore/DataStore but not from the in-memory cache (Android) or had no cache at all (JVM). A concurrent `encrypt()` call could re-cache the stale key before the delete completed, causing subsequent encryptions to use a key that no longer existed in persistent storage.
60
+
61
+
**Fix:**`deleteKey()` now holds the same per-alias lock as `getOrCreateKey()` and removes the key from both the persistent store and the in-memory cache atomically. Applied to both `AndroidKeystoreEncryption` and `JvmSoftwareEncryption`.
Both `AndroidKeystoreEncryption` and `JvmSoftwareEncryption` used `synchronized(identifier.intern())` for per-alias locking. `String.intern()` adds strings to the JVM's permanent string pool, which is never garbage collected. With dynamic key aliases (e.g., per-user keys), this caused unbounded memory growth.
66
+
67
+
**Fix:** Replaced with `ConcurrentHashMap<String, Any>` lock maps and a `lockFor(alias)` helper. Lock objects are scoped to the encryption engine instance and eligible for GC when the engine is collected.
68
+
69
+
### Removed
70
+
71
+
-**`IntegrityChecker`** — Removed from all platforms (Android, iOS, JVM, WASM). This was a wrapper around Google Play Integrity (Android) and Apple DeviceCheck (iOS) that generated tokens for server-side device verification. It had no connection to KSafe's core encrypted storage functionality and added transitive dependencies (`play-integrity`, `play-services-base`) to every consumer. Client-side root/jailbreak detection remains available via `SecurityChecker` and `KSafeSecurityPolicy`.
72
+
- Removed `play-integrity` and `play-services-base` dependencies from Android
73
+
74
+
### Changed
75
+
76
+
-`datastore-preferences-core` dependency moved from `commonMain` to per-platform source sets (Android, JVM, iOS) — WASM uses `localStorage` and does not depend on DataStore
77
+
78
+
### Added (Testing)
79
+
80
+
-`Jvm160FixesTest` — 9 new JVM tests covering the three encryption engine fixes:
81
+
-`testConcurrentEncryptedWritesSameKey_noDataLoss` — verifies concurrent encrypted writes to the same key all produce readable values
82
+
-`testConcurrentEncryptedWritesDifferentKeys_allReadable` — verifies concurrent writes to different keys don't interfere
83
+
-`testKeyGenerationRaceStress` — stress test with 20 threads writing to the same key
84
+
-`testDeleteKeyDoesNotLeaveStaleCache` — verifies deleted keys return default on re-read
85
+
-`testDeleteKeyRaceWithConcurrentEncryption` — verifies delete + encrypt race doesn't corrupt data
86
+
-`testRepeatedDeleteAndRewriteCycles` — 50 cycles of delete + rewrite with integrity checks
87
+
-`testManyUniqueAliasesWork` — 100 unique aliases to verify lock map scalability
88
+
-`testLockMapSerializesPerAlias` — verifies per-alias serialization with concurrent readers/writers
89
+
-`testDynamicStringAliasesShareLock` — verifies dynamically constructed alias strings share the same lock
90
+
-`WasmJsKSafeTest` — WASM test suite extending `KSafeTest` with `FakeEncryption` (WebCrypto requires browser)
91
+
92
+
### Dependencies
93
+
94
+
- Added `cryptography-provider-webcrypto` for WASM target
95
+
96
+
---
97
+
5
98
## [1.5.0] - 2025-02-09
6
99
7
100
### Added
@@ -428,29 +521,6 @@ Background: collect 16ms window → encrypt all → single DataStore.edit()
0 commit comments