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
Copy file name to clipboardExpand all lines: CHANGELOG.md
+4-11Lines changed: 4 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,6 +34,8 @@ val ksafe = KSafe(
34
34
- New `KSafeEncryption.updateKeyAccessibility()` interface method (default no-op, overridden on iOS)
35
35
- Migration marker `__ksafe_access_policy__` stored in DataStore (skipped by `updateCache()` on all platforms)
36
36
37
+
**Error behavior when locked:** When `requireUnlockedDevice = true` and the device is locked, encrypted reads (`getDirect`, `get`, `getFlow`) and suspend writes (`put`) throw `IllegalStateException` instead of silently returning default values. `putDirect` does not throw — the background write consumer logs the error and drops the batch while staying alive for future writes. On Android, `InvalidKeyException` from `Cipher.init()` is wrapped as `IllegalStateException("device is locked")` and propagated through `resolveFromCache` and `getEncryptedFlow`. Apps can catch this exception to detect and handle locked-device scenarios.
38
+
37
39
#### New Memory Policy: `ENCRYPTED_WITH_TIMED_CACHE`
38
40
39
41
A third memory policy that balances security and performance. The primary `memoryCache` still holds ciphertext (like `ENCRYPTED`), but a secondary plaintext cache stores recently-decrypted values for a configurable TTL.
@@ -109,20 +111,11 @@ private fun startBackgroundCollector() {
109
111
110
112
### Fixed
111
113
112
-
#### Locked-device exceptions now propagate correctly
113
-
114
-
When `requireUnlockedDevice = true` and the device is locked, encrypted reads (`getDirect`, `get`, `getFlow`) and suspend writes (`put`) now throw `IllegalStateException` instead of silently returning default values. `putDirect` does not throw to the caller — the background write consumer logs the error and drops the batch while staying alive for future writes.
114
+
#### iOS Keychain operations now check return values
115
115
116
-
**Android:**
117
-
-`encryptWithKey()` and `decryptWithKey()` now catch `InvalidKeyException` from `Cipher.init()` (thrown when `setUnlockedDeviceRequired(true)` keys are used while locked) and wrap it as `IllegalStateException("KSafe: Cannot access Keystore key - device is locked.")`
118
-
-`resolveFromCache` and `getEncryptedFlow` now re-throw `IllegalStateException` containing "device is locked" instead of swallowing it in the generic `catch (_: Exception)` handler
119
-
120
-
**iOS:**
121
-
-`storeInKeychain()` now checks the `SecItemAdd` return value — previously it was silently ignored, meaning key storage could fail while the device was locked without any error
116
+
-`storeInKeychain()` now checks the `SecItemAdd` return value — previously it was silently ignored, meaning key storage could fail without any error
122
117
-`updateKeyAccessibility()` now checks the `SecItemUpdate` return value and throws on failure
123
118
124
-
These fixes ensure that apps using `requireUnlockedDevice = true` can reliably detect and handle locked-device scenarios (e.g., showing a "device is locked" message) instead of silently returning default values.
125
-
126
119
#### Suspend API no longer blocks the calling dispatcher during encryption/decryption
127
120
128
121
The suspend functions `put(encrypted = true)` and `get(encrypted = true)` previously called `engine.encrypt()`/`engine.decrypt()` directly on the caller's coroutine dispatcher. If called from `Dispatchers.Main` (e.g., inside `viewModelScope.launch`), the blocking AES-GCM operation would run on the main thread. On Android, first-time Keystore key generation can take 50–200ms — enough to drop frames.
0 commit comments