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