简体   繁体   中英

Can't fetch saved account from Keychain

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 , ie the search is successful but the result is null although saveData() finds duplicates... I don't just understand already...

Finally, I found my mistake. SecItemCopyMatching won't return any results without kSecReturnData to kCFBooleanTrue attribute. So, the final method will look as below:

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`")
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM