Skip to content

Commit e2690e1

Browse files
committed
fix: XCFramework registration + iOS Keychain interop types
- Register XCFrameworkConfig in shared build so assembleSharedDebugXCFramework task exists - Rewrite iOS SecureStore to use CFDictionary APIs directly (CFDictionaryCreateMutable + CFDictionarySetValue + CFBridgingRetain) instead of NSMutableDictionary casts that fail Kotlin/Native type checking All three iOS targets (arm64, simulatorArm64, x64) now compile and link successfully.
1 parent 2fd1db2 commit e2690e1

2 files changed

Lines changed: 62 additions & 35 deletions

File tree

mobile/shared/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ kotlin {
2020
}
2121
}
2222

23+
val xcf = org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig(project, "Shared")
24+
2325
listOf(
2426
iosX64(),
2527
iosArm64(),
@@ -28,6 +30,7 @@ kotlin {
2830
target.binaries.framework {
2931
baseName = "Shared"
3032
isStatic = true
33+
xcf.add(this)
3134
}
3235
}
3336

mobile/shared/src/iosMain/kotlin/app/myfaq/shared/platform/SecureStore.ios.kt

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
package app.myfaq.shared.platform
22

3-
import kotlinx.cinterop.BetaInteropApi
43
import 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
518
import platform.Foundation.NSData
619
import platform.Foundation.NSString
720
import platform.Foundation.NSUTF8StringEncoding
821
import platform.Foundation.create
922
import platform.Foundation.dataUsingEncoding
10-
import platform.Foundation.NSMutableDictionary
1123
import platform.Security.SecItemAdd
1224
import platform.Security.SecItemCopyMatching
1325
import platform.Security.SecItemDelete
@@ -21,63 +33,75 @@ import platform.Security.kSecMatchLimit
2133
import platform.Security.kSecMatchLimitOne
2234
import platform.Security.kSecReturnData
2335
import 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)
3644
actual 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

Comments
 (0)