How to resolve Xamarin iOS SecKeyChain Interaction

2019-07-30 00:08发布

问题:

In my Xamarin.iOS project I used SecRecord/SecKeyChain to store my token values and app version. From production log I found keychain related exceptions with status code 'InteractionNotAllowed' when try to write/read items in keychain. Apple documents states that to resolve InteractionNotAllowed error we need to change the default kSecAttrAccessible attribute value from ‘WhenUnlocked' to ‘Always’. But in my existing code when I changed accessible attribute to 'Always' app log out because it failed to read token from keychain. It return’s 'Item not found' when read. But when I tried to save token again it returns 'Duplicate item'. So again I tried to remove same item but this time it again returns 'Item not found'. That’s really strange I can’t delete it and I can’t read it with same key.

Below is the code snippet -

private SecRecord CreateRecordForNewKeyValue(string accountName, string value)
        {
            return new SecRecord(SecKind.GenericPassword)
            {
                Service = App.AppName,
                Account = accountName,
                ValueData = NSData.FromString(value, NSStringEncoding.UTF8),
                Accessible = SecAccessible.Always //This line of code is newly added.
            };
        }



private SecRecord ExistingRecordForKey(string accountName)
        {
            return new SecRecord(SecKind.GenericPassword)
            {
                Service = App.AppName,
                Account = accountName,
                Accessible = SecAccessible.Always //This line of code is newly added. 
            };
        }



public void SetValueForKeyAndAccount(string value, string accountName, string key)
        {
            var record = ExistingRecordForKey(accountName);
            try
            {
                if (string.IsNullOrEmpty(value))
                {
                    if (!string.IsNullOrEmpty(GetValueFromAccountAndKey(accountName, key)))
                        RemoveRecord(record);
                    return;
                }
                // if the key already exists, remove it before set value
                if (!string.IsNullOrEmpty(GetValueFromAccountAndKey(accountName, key)))
                    RemoveRecord(record);
            }
            catch (Exception e)
            {
                //Log exception here -("RemoveRecord Failed " + accountName, e,);
            }
            //Adding new record values to keychain
            var result = SecKeyChain.Add(CreateRecordForNewKeyValue(accountName, value));
            if (result != SecStatusCode.Success)
            {
                if (result == SecStatusCode.DuplicateItem)
                {
                    try
                    {
                        //Log exception here -("Error adding record: {0} for Account-" + accountName, result), "Try Remove account");
                        RemoveRecord(record);
                    }
                    catch (Exception e)
                    {
                        //Log exception here -("RemoveRecord Failed  after getting error SecStatusCode.DuplicateItem for Account-" + accountName, e);
                    } 
                }
                else
                    throw new Exception(string.Format("Error adding record: {0} for Account-" + accountName, result));
            }
        }    


public string GetValueFromAccountAndKey(string accountName, string key)
        {
            try
            {
                var record = ExistingRecordForKey(accountName);
                SecStatusCode resultCode;
                var match = SecKeyChain.QueryAsRecord(record, out resultCode);
                if (resultCode == SecStatusCode.Success)
                {
                    if (match.ValueData != null)
                    {
                        string valueData = NSString.FromData(match.ValueData, NSStringEncoding.UTF8);
                        if (string.IsNullOrEmpty(valueData))
                            return string.Empty;
                        return valueData;
                    }
                    else if (match.Generic != null)
                    {
                        string valueData = NSString.FromData(match.ValueData, NSStringEncoding.UTF8);
                        if (string.IsNullOrEmpty(valueData))
                            return string.Empty;
                        return valueData;
                    }
                    else
                        return string.Empty;
                }
            }
            catch (Exception e)
            {
                // Exception logged here -("iOS Keychain Error for account-" + accountName, e);
            }
            return string.Empty;
        }

Any help would be great! Thanks

回答1:

The property Service is also an unique identification when we store or retrieve data using KeyChain. You didn't post your GetValueFromAccountAndKey() method, so we don't know what is the key used for? But in your case, you should use the same Service to retrieve value:

string GetValueFromAccountAndKey(string accoundName, string service)
{
    var securityRecord = new SecRecord(SecKind.GenericPassword)
    {
        Service = service,
        Account = accoundName
    };

    SecStatusCode status;
    NSData resultData = SecKeyChain.QueryAsData(securityRecord, false, out status);

    var result = resultData != null ? new NSString(resultData, NSStringEncoding.UTF8) : "Not found";

    return result;
}

Since you just make a hard code in your CreateRecordForNewKeyValue()( the Service has been written as a constant ), if you want to retrieve your value you should also set the Service as App.AppName in the method GetValueFromAccountAndKey().

It return’s 'Item not found' when read. But when I tried to save token again it returns 'Duplicate item'.

This is because when we use the same Account but different Service to retrieve data, KeyChain can't find the corresponding SecRecord. This made you thought the SecRecord didn't exist, then use the same Account to store value. The Duplicate item result throws out. For a SecRecord, the Account and Service must both be unique.