Search code examples
iosxamarin.ioskeychainappauth

Xamarin.iOS Keychain hangs when trying to query for a key storing NSData


I have an existing enterprise deployed application that uses OpenID Connect for authentication. Once authenticated, I store the AuthState object in the iOS keychain for security and to be able to sign the user in later with just Face/Touch ID (given the refresh token in AuthState is still valid). The AuthState object looks like this:

namespace OpenId.AppAuth
{
    [Register ("OIDAuthState", true)]
    public class AuthState : NSObject, INSCoding, INativeObject, IDisposable, INSSecureCoding
    {
        [CompilerGenerated]
        private static readonly IntPtr class_ptr = Class.GetHandle ("OIDAuthState");

        [CompilerGenerated]
        private object __mt_ErrorDelegate_var;

        [CompilerGenerated]
        private object __mt_StateChangeDelegate_var;

        public override IntPtr ClassHandle {
            get;
        }

        [CompilerGenerated]
        public virtual NSError AuthorizationError {
            [Export ("authorizationError")]
            get;
        }

        [CompilerGenerated]
        public virtual IAuthStateErrorDelegate ErrorDelegate {
            [Export ("errorDelegate", ArgumentSemantic.Weak)]
            get;
            [Export ("setErrorDelegate:", ArgumentSemantic.Weak)]
            set;
        }

        [CompilerGenerated]
        public virtual bool IsAuthorized {
            [Export ("isAuthorized")]
            get;
        }

        [CompilerGenerated]
        public virtual AuthorizationResponse LastAuthorizationResponse {
            [Export ("lastAuthorizationResponse")]
            get;
        }

        [CompilerGenerated]
        public virtual RegistrationResponse LastRegistrationResponse {
            [Export ("lastRegistrationResponse")]
            get;
        }

        [CompilerGenerated]
        public virtual TokenResponse LastTokenResponse {
            [Export ("lastTokenResponse")]
            get;
        }

        [CompilerGenerated]
        public virtual string RefreshToken {
            [Export ("refreshToken")]
            get;
        }

        [CompilerGenerated]
        public virtual string Scope {
            [Export ("scope")]
            get;
        }

        [CompilerGenerated]
        public virtual IAuthStateChangeDelegate StateChangeDelegate {
            [Export ("stateChangeDelegate", ArgumentSemantic.Weak)]
            get;
            [Export ("setStateChangeDelegate:", ArgumentSemantic.Weak)]
            set;
        }

        public static IAuthorizationFlowSession PresentAuthorizationRequest (AuthorizationRequest authorizationRequest, UIViewController presentingViewController, AuthStateAuthorizationCallback callback);

        [CompilerGenerated]
        [DesignatedInitializer]
        [EditorBrowsable (EditorBrowsableState.Advanced)]
        [Export ("initWithCoder:")]
        public AuthState (NSCoder coder)
            : base (NSObjectFlag.Empty);

        [CompilerGenerated]
        [EditorBrowsable (EditorBrowsableState.Advanced)]
        protected AuthState (NSObjectFlag t)
            : base (t);

        [CompilerGenerated]
        [EditorBrowsable (EditorBrowsableState.Advanced)]
        protected internal AuthState (IntPtr handle)
            : base (handle);

        [Export ("initWithAuthorizationResponse:")]
        [CompilerGenerated]
        public AuthState (AuthorizationResponse authorizationResponse)
            : base (NSObjectFlag.Empty);

        [Export ("initWithAuthorizationResponse:tokenResponse:")]
        [CompilerGenerated]
        public AuthState (AuthorizationResponse authorizationResponse, TokenResponse tokenResponse)
            : base (NSObjectFlag.Empty);

        [Export ("initWithRegistrationResponse:")]
        [CompilerGenerated]
        public AuthState (RegistrationResponse registrationResponse)
            : base (NSObjectFlag.Empty);

        [Export ("initWithAuthorizationResponse:tokenResponse:registrationResponse:")]
        [DesignatedInitializer]
        [CompilerGenerated]
        public AuthState (AuthorizationResponse authorizationResponse, TokenResponse tokenResponse, RegistrationResponse registrationResponse)
            : base (NSObjectFlag.Empty);

        [Export ("encodeWithCoder:")]
        [CompilerGenerated]
        [Preserve (Conditional = true)]
        public virtual void EncodeTo (NSCoder encoder);

        [Export ("performActionWithFreshTokens:")]
        [CompilerGenerated]
        public unsafe virtual void PerformWithFreshTokens ([BlockProxy (typeof(ObjCRuntime.Trampolines.NIDAuthStateAction))] AuthStateAction action);

        [Export ("performActionWithFreshTokens:additionalRefreshParameters:")]
        [CompilerGenerated]
        public unsafe virtual void PerformWithFreshTokens ([BlockProxy (typeof(ObjCRuntime.Trampolines.NIDAuthStateAction))] AuthStateAction action, NSDictionary<NSString, NSString> additionalParameters);

        [Export ("authStateByPresentingAuthorizationRequest:UICoordinator:callback:")]
        [CompilerGenerated]
        public unsafe static IAuthorizationFlowSession PresentAuthorizationRequest (AuthorizationRequest authorizationRequest, IAuthorizationUICoordinator UICoordinator, [BlockProxy (typeof(ObjCRuntime.Trampolines.NIDAuthStateAuthorizationCallback))] AuthStateAuthorizationCallback callback);

        [Export ("setNeedsTokenRefresh")]
        [CompilerGenerated]
        public virtual void SetNeedsTokenRefresh ();

        [Export ("tokenRefreshRequest")]
        [CompilerGenerated]
        public virtual TokenRequest TokenRefreshRequest ();

        [Export ("tokenRefreshRequestWithAdditionalParameters:")]
        [CompilerGenerated]
        public virtual TokenRequest TokenRefreshRequest (NSDictionary<NSString, NSString> additionalParameters);

        [Export ("updateWithAuthorizationResponse:error:")]
        [CompilerGenerated]
        public virtual void Update (AuthorizationResponse authorizationResponse, NSError error);

        [Export ("updateWithTokenResponse:error:")]
        [CompilerGenerated]
        public virtual void Update (TokenResponse tokenResponse, NSError error);

        [Export ("updateWithAuthorizationError:")]
        [CompilerGenerated]
        public virtual void Update (NSError authorizationError);

        [Export ("updateWithRegistrationResponse:")]
        [CompilerGenerated]
        public virtual void UpdateWithRegistrationResponse (RegistrationResponse registrationResponse);

        [CompilerGenerated]
        protected override void Dispose (bool disposing);
    }
}

Prior to iOS 12.1 update everything worked perfectly fine

PROBLEM: after installing the ios 12.1 update, only when running in release mode (works when debugging in develop mode while connected to VS for Mac debugger) querying for an existing key in the keychain storing binary NSData representation of this AuthState object hangs and eventually the application is terminated for being un-responsive for longer than 10 seconds.

Has anyone run into any similar issues? Would be amazing if anyone could shed any light on what might be going on here or point me in the right direction.

Additional information:

How I get NSData binary representation of AppAuth object:

NSData authStateData = NSKeyedArchiver.ArchivedDataWithRootObject(authState);

How I save this NSData into keychain:

    var secAccess = new SecAccessControl(SecAccessible.WhenUnlockedThisDeviceOnly, SecAccessControlCreateFlags.UserPresence);
    var secRecord = new SecRecord(SecKind.GenericPassword)
    {
        Account = "keystring",
        Service = "ServiceNameString",
        Label = "keystring",
        ValueData = authStateData,
        AccessControl = secAccess
    };
    var result = SecKeyChain.Add(secRecord);

How I query for existing data in keychain:

var searchRecord = new SecRecord(SecKind.GenericPassword)
{
    Service = ServiceName,
    Label = key,
};
var match = SecKeyChain.QueryAsRecord(searchRecord, out SecStatusCode resultCode);

There are no obvious errors in the device log, I checked. Was there a change in iOS 12.1 that I missed that significantly affects this?

UPDATE: I refactored the code to only store an encryption key password in the keychain instead of an entire AuthState object and store the serialized AuthState encrypted into a local file instead. Still seeing the same issue, in debug mode on a device everything works, writes and reads from keychain just fine, when running without debugger attached on the same device, same build, writes fine, when reading hangs after validating TouchID/FaceID successfully, is there a bug in the Xamarin.iOS SDK that hasn't caught up to some change in the latest iOS that's causing this?


Solution

  • I've fixed the problem i was having, it seems to have been some kind of race condition that was locking the app as the call to save into keychain was being done on the main thread. Normally this call is very quick and never had a problem locking anything up in the past, but something in the iOS 12.1 update changed that. In any case, I simply explicitly ran code saving the encryption password into keychain in a background thread and that fixed the problem:

    Task.Run(() =>
    {
        var keychain = new KeyChain();
        keychain.SetValueForKey("securedvalue", "securedvaluekey");
    }).ConfigureAwait(false);