1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXSession.h" 4#import "EXUnversioned.h" 5 6NSString * const kEXSessionKeychainKey = @"host.exp.exponent.session"; 7NSString * const kEXSessionKeychainService = @"app"; 8 9@interface EXSession () 10 11@property (nonatomic, strong) NSDictionary *session; 12 13@end 14 15@implementation EXSession 16 17+ (nonnull instancetype)sharedInstance 18{ 19 static EXSession *theSession; 20 static dispatch_once_t once; 21 dispatch_once(&once, ^{ 22 if (!theSession) { 23 theSession = [[EXSession alloc] init]; 24 } 25 }); 26 return theSession; 27} 28 29- (NSDictionary * _Nullable)session 30{ 31 if (_session) { 32 return _session; 33 } 34 NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{ 35 (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne, 36 (__bridge id)kSecReturnData:(__bridge id)kCFBooleanTrue 37 }]; 38 [query addEntriesFromDictionary:[self _searchQuery]]; 39 40 CFTypeRef foundDict = NULL; 41 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &foundDict); 42 43 if (status == noErr) { 44 NSData *result = (__bridge_transfer NSData *)foundDict; 45 NSError *jsonError; 46 id session = [NSJSONSerialization JSONObjectWithData:result 47 options:kNilOptions 48 error:&jsonError]; 49 if (!jsonError && [session isKindOfClass:[NSDictionary class]]) { 50 return (NSDictionary *)session; 51 } 52 } 53 return nil; 54} 55 56- (NSString * _Nullable)sessionSecret 57{ 58 NSDictionary *session = [self session]; 59 if (!session) { 60 return nil; 61 } 62 63 id sessionSecret = session[@"sessionSecret"]; 64 if (sessionSecret && [sessionSecret isKindOfClass:[NSString class]]) { 65 return (NSString *)sessionSecret; 66 } 67 return nil; 68} 69 70- (BOOL)saveSessionToKeychain:(NSDictionary *)session error:(NSError **)error 71{ 72 NSError *jsonError; 73 NSData *encodedData = [NSJSONSerialization dataWithJSONObject:session 74 options:kNilOptions 75 error:&jsonError]; 76 if (jsonError) { 77 if (error) { 78 *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") 79 code:-1 80 userInfo:@{ 81 NSLocalizedDescriptionKey: @"Could not serialize JSON to save session to keychain", 82 NSUnderlyingErrorKey: jsonError 83 }]; 84 } 85 return NO; 86 } 87 88 NSDictionary *searchQuery = [self _searchQuery]; 89 NSDictionary *updateQuery = @{ (__bridge id)kSecValueData:encodedData }; 90 NSMutableDictionary *addQuery = [NSMutableDictionary dictionaryWithDictionary:searchQuery]; 91 [addQuery addEntriesFromDictionary:updateQuery]; 92 93 OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL); 94 95 if (status == errSecDuplicateItem) { 96 status = SecItemUpdate((__bridge CFDictionaryRef)searchQuery, (__bridge CFDictionaryRef)updateQuery); 97 } 98 99 if (status == errSecSuccess) { 100 _session = session; 101 return YES; 102 } else { 103 if (error) { 104 *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") 105 code:-1 106 userInfo:@{ NSLocalizedDescriptionKey: @"Could not save session to keychain" }]; 107 } 108 return NO; 109 } 110} 111 112- (BOOL)deleteSessionFromKeychainWithError:(NSError **)error 113{ 114 OSStatus status = SecItemDelete((__bridge CFDictionaryRef)[self _searchQuery]); 115 116 if (status == errSecSuccess) { 117 _session = nil; 118 return YES; 119 } else { 120 if (error) { 121 *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") 122 code:-1 123 userInfo:@{ NSLocalizedDescriptionKey: @"Could not delete session from keychain" }]; 124 } 125 return NO; 126 } 127} 128 129- (NSDictionary *)_searchQuery 130{ 131 NSData *encodedKey = [kEXSessionKeychainKey dataUsingEncoding:NSUTF8StringEncoding]; 132 return @{ 133 (__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword, 134 (__bridge id)kSecAttrService:kEXSessionKeychainService, 135 (__bridge id)kSecAttrGeneric:encodedKey, 136 (__bridge id)kSecAttrAccount:encodedKey 137 }; 138} 139 140@end 141