11package app.myfaq.shared.platform
22
3- import kotlinx.cinterop.BetaInteropApi
43import kotlinx.cinterop.ExperimentalForeignApi
4+ import kotlinx.cinterop.alloc
5+ import kotlinx.cinterop.memScoped
6+ import kotlinx.cinterop.ptr
7+ import kotlinx.cinterop.value
8+ import platform.CoreFoundation.CFDictionaryCreateMutable
9+ import platform.CoreFoundation.CFDictionaryRef
10+ import platform.CoreFoundation.CFDictionarySetValue
11+ import platform.CoreFoundation.CFMutableDictionaryRef
12+ import platform.CoreFoundation.CFTypeRefVar
13+ import platform.CoreFoundation.kCFAllocatorDefault
14+ import platform.CoreFoundation.kCFTypeDictionaryKeyCallBacks
15+ import platform.CoreFoundation.kCFTypeDictionaryValueCallBacks
16+ import platform.Foundation.CFBridgingRelease
17+ import platform.Foundation.CFBridgingRetain
518import platform.Foundation.NSData
619import platform.Foundation.NSString
720import platform.Foundation.NSUTF8StringEncoding
821import platform.Foundation.create
922import platform.Foundation.dataUsingEncoding
10- import platform.Foundation.NSMutableDictionary
1123import platform.Security.SecItemAdd
1224import platform.Security.SecItemCopyMatching
1325import platform.Security.SecItemDelete
@@ -21,63 +33,75 @@ import platform.Security.kSecMatchLimit
2133import platform.Security.kSecMatchLimitOne
2234import platform.Security.kSecReturnData
2335import platform.Security.kSecValueData
24- import platform.darwin.NSObject
36+ import platform.Security.errSecSuccess
2537
2638/* *
2739 * iOS [SecureStore] backed by Keychain Services with
2840 * `kSecAttrAccessibleAfterFirstUnlock` so values survive reboots
2941 * but are unavailable until the user unlocks the device once.
30- *
31- * Service: `app.myfaq.ios`. Callers namespace their keys by
32- * instance UUID. `clear()` enumerates by service tag so a user-
33- * initiated "wipe" removes every per-instance secret atomically.
3442 */
35- @OptIn(ExperimentalForeignApi ::class , BetaInteropApi :: class )
43+ @OptIn(ExperimentalForeignApi ::class )
3644actual class SecureStore {
3745
3846 actual fun put (key : String , value : String ) {
3947 remove(key)
40- val data = (value as NSString ).dataUsingEncoding(NSUTF8StringEncoding )
41- ? : return
42- val add = baseQuery(key).apply {
43- setObject(data, kSecValueData as NSObject )
44- setObject(kSecAttrAccessibleAfterFirstUnlock as NSObject , kSecAttrAccessible as NSObject )
48+ val data = (value as NSString ).dataUsingEncoding(NSUTF8StringEncoding ) ? : return
49+
50+ val query = cfMutableDict(6 ).apply {
51+ cfSet(kSecClass, kSecClassGenericPassword)
52+ cfSet(kSecAttrService, CFBridgingRetain (SERVICE ))
53+ cfSet(kSecAttrAccount, CFBridgingRetain (key))
54+ cfSet(kSecValueData, CFBridgingRetain (data))
55+ cfSet(kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock)
4556 }
46- SecItemAdd (add , null )
57+ SecItemAdd (query as CFDictionaryRef , null )
4758 }
4859
4960 actual fun get (key : String ): String? {
50- val query = baseQuery(key).apply {
51- setObject(kSecMatchLimitOne as NSObject , kSecMatchLimit as NSObject )
52- setObject(true as NSObject , kSecReturnData as NSObject )
61+ val query = cfMutableDict(5 ).apply {
62+ cfSet(kSecClass, kSecClassGenericPassword)
63+ cfSet(kSecAttrService, CFBridgingRetain (SERVICE ))
64+ cfSet(kSecAttrAccount, CFBridgingRetain (key))
65+ cfSet(kSecMatchLimit, kSecMatchLimitOne)
66+ cfSet(kSecReturnData, CFBridgingRetain (true ))
67+ }
68+
69+ memScoped {
70+ val result = alloc<CFTypeRefVar >()
71+ val status = SecItemCopyMatching (query as CFDictionaryRef , result.ptr)
72+ if (status != errSecSuccess) return null
73+ val data = CFBridgingRelease (result.value) as ? NSData ? : return null
74+ return NSString .create(data, NSUTF8StringEncoding ) as ? String
5375 }
54- val result = kotlinx.cinterop.memScoped {
55- val ref = kotlinx.cinterop.alloc< kotlinx.cinterop.CPointerVar < platform.CoreFoundation .__CFType >> ()
56- val status = SecItemCopyMatching (query, ref.ptr)
57- if (status != 0 ) return @memScoped null
58- val data = ref.value ? : return @memScoped null
59- @Suppress(" UNCHECKED_CAST" )
60- (data as ? NSData )
61- } ? : return null
62- return NSString .create(result, NSUTF8StringEncoding ) as String?
6376 }
6477
6578 actual fun remove (key : String ) {
66- SecItemDelete (baseQuery(key))
79+ val query = cfMutableDict(3 ).apply {
80+ cfSet(kSecClass, kSecClassGenericPassword)
81+ cfSet(kSecAttrService, CFBridgingRetain (SERVICE ))
82+ cfSet(kSecAttrAccount, CFBridgingRetain (key))
83+ }
84+ SecItemDelete (query as CFDictionaryRef )
6785 }
6886
6987 actual fun clear () {
70- val query = NSMutableDictionary ( ).apply {
71- setObject(kSecClassGenericPassword as NSObject , kSecClass as NSObject )
72- setObject( SERVICE as NSObject , kSecAttrService as NSObject )
88+ val query = cfMutableDict( 2 ).apply {
89+ cfSet(kSecClass, kSecClassGenericPassword )
90+ cfSet(kSecAttrService, CFBridgingRetain ( SERVICE ) )
7391 }
74- SecItemDelete (query)
92+ SecItemDelete (query as CFDictionaryRef )
7593 }
7694
77- private fun baseQuery (account : String ): NSMutableDictionary = NSMutableDictionary ().apply {
78- setObject(kSecClassGenericPassword as NSObject , kSecClass as NSObject )
79- setObject(SERVICE as NSObject , kSecAttrService as NSObject )
80- setObject(account as NSObject , kSecAttrAccount as NSObject )
95+ private fun cfMutableDict (capacity : Int ): CFMutableDictionaryRef =
96+ CFDictionaryCreateMutable (
97+ kCFAllocatorDefault,
98+ capacity.toLong(),
99+ kCFTypeDictionaryKeyCallBacks.ptr,
100+ kCFTypeDictionaryValueCallBacks.ptr,
101+ )!!
102+
103+ private fun CFMutableDictionaryRef.cfSet (key : Any? , value : Any? ) {
104+ CFDictionarySetValue (this , CFBridgingRetain (key), CFBridgingRetain (value))
81105 }
82106
83107 private companion object {
0 commit comments