// Copyright 2015-present 650 Industries. All rights reserved. #import "EXSession.h" #import "EXUnversioned.h" NSString * const kEXSessionKeychainKey = @"host.exp.exponent.session"; NSString * const kEXSessionKeychainService = @"app"; @interface EXSession () @property (nonatomic, strong) NSDictionary *session; @end @implementation EXSession + (nonnull instancetype)sharedInstance { static EXSession *theSession; static dispatch_once_t once; dispatch_once(&once, ^{ if (!theSession) { theSession = [[EXSession alloc] init]; } }); return theSession; } - (NSDictionary * _Nullable)session { if (_session) { return _session; } NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{ (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne, (__bridge id)kSecReturnData:(__bridge id)kCFBooleanTrue }]; [query addEntriesFromDictionary:[self _searchQuery]]; CFTypeRef foundDict = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &foundDict); if (status == noErr) { NSData *result = (__bridge_transfer NSData *)foundDict; NSError *jsonError; id session = [NSJSONSerialization JSONObjectWithData:result options:kNilOptions error:&jsonError]; if (!jsonError && [session isKindOfClass:[NSDictionary class]]) { return (NSDictionary *)session; } } return nil; } - (NSString * _Nullable)sessionSecret { NSDictionary *session = [self session]; if (!session) { return nil; } id sessionSecret = session[@"sessionSecret"]; if (sessionSecret && [sessionSecret isKindOfClass:[NSString class]]) { return (NSString *)sessionSecret; } return nil; } - (BOOL)saveSessionToKeychain:(NSDictionary *)session error:(NSError **)error { NSError *jsonError; NSData *encodedData = [NSJSONSerialization dataWithJSONObject:session options:kNilOptions error:&jsonError]; if (jsonError) { if (error) { *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Could not serialize JSON to save session to keychain", NSUnderlyingErrorKey: jsonError }]; } return NO; } NSDictionary *searchQuery = [self _searchQuery]; NSDictionary *updateQuery = @{ (__bridge id)kSecValueData:encodedData }; NSMutableDictionary *addQuery = [NSMutableDictionary dictionaryWithDictionary:searchQuery]; [addQuery addEntriesFromDictionary:updateQuery]; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL); if (status == errSecDuplicateItem) { status = SecItemUpdate((__bridge CFDictionaryRef)searchQuery, (__bridge CFDictionaryRef)updateQuery); } if (status == errSecSuccess) { _session = session; return YES; } else { if (error) { *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Could not save session to keychain" }]; } return NO; } } - (BOOL)deleteSessionFromKeychainWithError:(NSError **)error { OSStatus status = SecItemDelete((__bridge CFDictionaryRef)[self _searchQuery]); if (status == errSecSuccess || status == errSecItemNotFound) { _session = nil; return YES; } else { if (error) { *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Could not delete session from keychain" }]; } return NO; } } - (NSDictionary *)_searchQuery { NSData *encodedKey = [kEXSessionKeychainKey dataUsingEncoding:NSUTF8StringEncoding]; return @{ (__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService:kEXSessionKeychainService, (__bridge id)kSecAttrGeneric:encodedKey, (__bridge id)kSecAttrAccount:encodedKey }; } @end