我使用 Apple Keychain 对 iOS 的客户经理做了一个简单的模拟:
@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"
}
}
经理正确保存帐户,至少我认为是因为在第二次通话时我得到了 errSecDuplicateItem
状态。但是,当我调用 getAccount
时,它总是返回 null
。
我的代码有什么问题?
UPD
我改进了 getData()
一点:
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`")
}
}
它崩溃并出现异常:Error while searching for an account, result is null
,即搜索成功但结果是 null
虽然 saveData()
找到重复项......我不只是明白......
回答1
最后,我发现了我的错误。 SecItemCopyMatching
不会返回没有 kSecReturnData to kCFBooleanTrue
属性的任何结果。因此,最终方法如下所示:
private fun getData(): String? {
memScoped {
val query = query(
kSecClass to kSecClassGenericPassword,
kSecAttrService to SERVICE.toCFDict(),
kSecAttrAccount to ACCOUNT_TYPE.toCFDict(),
kSecReturnData to kCFBooleanTrue,
)
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`")
}
}