I made a simple analog of the account manager for iOS using Apple Keychain:
@Serializable
data class Account(
val name: String,
val password: String,
val accessToken: String,
val refreshToken: String,
)
actual class AccountManagerGateway : IAccountManagerGateway {
private val json = Json { encodeDefaults = true }
override fun addAccount(account: Account) = updateAccount(account)
override fun updateAccount(account: Account) {
val data = json.encodeToString(account)
if (!saveData(data)) {
throw AccountException("Account not saved")
}
}
override fun getAccount(): Account? = this.getData()?.let {
json.decodeFromString(it)
}
override fun deleteAccount() {
deleteData()
}
private fun getData(): String? {
memScoped {
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrService to SERVICE.toCFDict(),
kSecAttrAccount to ACCOUNT_TYPE.toCFDict(),
)
val result = alloc<CFTypeRefVar>()
val status = SecItemCopyMatching(query, result.ptr)
if (status == errSecSuccess && result.value != null) {
val value = CFBridgingRelease(result.value) as? NSData
return value?.stringValue
}
return null
}
}
private fun deleteData(): Boolean {
memScoped {
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrService to SERVICE.toCFDict(),
kSecAttrAccount to ACCOUNT_TYPE.toCFDict(),
)
val status = SecItemDelete(query)
return status == errSecSuccess || status == errSecItemNotFound
}
}
private fun saveData(value: String): Boolean {
memScoped {
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrService to SERVICE.toCFDict(),
kSecAttrAccount to ACCOUNT_TYPE.toCFDict(),
kSecValueData to value.toCFDict(),
)
var status = SecItemAdd(query, null)
if (status == errSecDuplicateItem) {
SecItemDelete(query)
status = SecItemAdd(query, null)
}
return status == errSecSuccess
}
}
private fun query(vararg pairs: Pair<CValuesRef<*>?, CValuesRef<*>?>): CFMutableDictionaryRef? {
memScoped {
val dict = CFDictionaryCreateMutable(null, pairs.size.toLong(), null, null)
pairs.forEach {
CFDictionaryAddValue(dict, it.first, it.second)
}
return dict
}
}
private fun String.toCFDict(): CFTypeRef? {
memScoped {
return CFBridgingRetain(
NSData.dataWithBytes(
bytes = this@toCFDict.cstr.ptr,
length = this@toCFDict.length.toULong()
)
)
}
}
private val NSData.stringValue: String?
get() = NSString.create(this, NSUTF8StringEncoding) as String?
companion object {
const val ACCOUNT_TYPE = "com.myapp"
const val SERVICE = "www.myapp.com"
}
}
The manager saves accounts correctly, at least I think so cuz on the second call I get the errSecDuplicateItem
status. However, when I call getAccount
it always returns null
.
What's the problem with my code?
UPD
I've improved getData()
a little:
private fun getData(): String? {
memScoped {
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrService to SERVICE.toCFDict(),
kSecAttrAccount to ACCOUNT_TYPE.toCFDict(),
)
val result = alloc<CFTypeRefVar>()
val status = SecItemCopyMatching(query, result.ptr)
when (status) {
errSecSuccess -> {
if (result.value != null) {
val value = CFBridgingRelease(result.value) as? NSData
return value?.stringValue
} else {
throw AccountException(
"Error while searching for an account, result is null"
)
}
}
errSecItemNotFound -> return null
}
throw AccountException("Error while searching for an account, status: `$status`")
}
}
It crashes with exception: Error while searching for an account, result is null
, i.e. the search is successful but the result is null
although saveData()
finds duplicates... I don't just understand already...