1// Copyright 2018-present 650 Industries. All rights reserved.
2
3#import <EXNotifications/EXServerRegistrationModule.h>
4
5static NSString * const kEXDeviceInstallationUUIDKey = @"EXDeviceInstallationUUIDKey";
6static NSString * const kEXDeviceInstallationUUIDLegacyKey = @"EXDeviceInstallUUIDKey";
7
8static NSString * const kEXRegistrationInfoKey = @"EXNotificationRegistrationInfoKey";
9
10@implementation EXServerRegistrationModule
11
12EX_EXPORT_MODULE(NotificationsServerRegistrationModule)
13
14EX_EXPORT_METHOD_AS(getInstallationIdAsync,
15                    getInstallationIdAsyncWithResolver:(EXPromiseResolveBlock)resolve
16                                              rejecter:(EXPromiseRejectBlock)reject)
17{
18  resolve([self getInstallationId]);
19}
20
21- (NSString *)getInstallationId
22{
23  NSString *installationId = [self fetchInstallationId];
24  if (installationId) {
25    return installationId;
26  }
27
28  installationId = [[NSUUID UUID] UUIDString];
29  [self setInstallationId:installationId error:NULL];
30  return installationId;
31}
32
33- (nullable NSString *)fetchInstallationId
34{
35  NSString *installationId;
36  CFTypeRef keychainResult = NULL;
37
38  if (SecItemCopyMatching((__bridge CFDictionaryRef)[self installationIdGetQuery], &keychainResult) == noErr) {
39    NSData *result = (__bridge_transfer NSData *)keychainResult;
40    NSString *value = [[NSString alloc] initWithData:result
41                                            encoding:NSUTF8StringEncoding];
42    // `initWithUUIDString` returns nil if string is not a valid UUID
43    if ([[NSUUID alloc] initWithUUIDString:value]) {
44      installationId = value;
45    }
46  }
47
48  if (installationId) {
49    return installationId;
50  }
51
52  NSString *legacyUUID = [[NSUserDefaults standardUserDefaults] stringForKey:kEXDeviceInstallationUUIDLegacyKey];
53  if (legacyUUID) {
54    installationId = legacyUUID;
55
56    NSError *error = nil;
57    if ([self setInstallationId:installationId error:&error]) {
58      // We only remove the value from old storage once it's set and saved in the new storage.
59      [[NSUserDefaults standardUserDefaults] removeObjectForKey:kEXDeviceInstallationUUIDLegacyKey];
60    } else {
61      NSLog(@"Could not migrate device installation UUID from legacy storage: %@", error.description);
62    }
63  }
64
65  return installationId;
66}
67
68- (BOOL)setInstallationId:(NSString *)installationId error:(NSError **)error
69{
70  // Delete existing UUID so we don't need to handle "duplicate item" error
71  SecItemDelete((__bridge CFDictionaryRef)[self installationIdSearchQuery]);
72
73  OSStatus status = SecItemAdd((__bridge CFDictionaryRef)[self installationIdSetQuery:installationId], NULL);
74  if (status != errSecSuccess && error) {
75    *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
76  }
77  return status == errSecSuccess;
78}
79
80# pragma mark - Keychain dictionaries
81
82- (NSDictionary *)keychainSearchQueryFor:(NSString *)key merging:(NSDictionary *)dictionaryToMerge
83{
84  NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
85  NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{
86    (__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword,
87    (__bridge id)kSecAttrService:[NSBundle mainBundle].bundleIdentifier,
88    (__bridge id)kSecAttrGeneric:encodedKey,
89    (__bridge id)kSecAttrAccount:encodedKey
90  }];
91  [query addEntriesFromDictionary:dictionaryToMerge];
92  return query;
93}
94
95# pragma mark Installation ID
96
97- (NSDictionary *)installationIdSearchQueryMerging:(NSDictionary *)dictionaryToMerge
98{
99  return [self keychainSearchQueryFor:kEXDeviceInstallationUUIDKey merging:dictionaryToMerge];
100}
101
102- (NSDictionary *)installationIdSearchQuery
103{
104  return [self installationIdSearchQueryMerging:@{}];
105}
106
107- (NSDictionary *)installationIdGetQuery
108{
109  return [self installationIdSearchQueryMerging:@{
110    (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne,
111    (__bridge id)kSecReturnData:(__bridge id)kCFBooleanTrue
112  }];
113}
114
115- (NSDictionary *)installationIdSetQuery:(NSString *)deviceInstallationUUID
116{
117  return [self installationIdSearchQueryMerging:@{
118    (__bridge id)kSecValueData:[deviceInstallationUUID dataUsingEncoding:NSUTF8StringEncoding],
119    (__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
120  }];
121}
122
123# pragma mark Registration information
124
125- (NSDictionary *)registrationSearchQueryMerging:(NSDictionary *)dictionaryToMerge
126{
127  return [self keychainSearchQueryFor:kEXRegistrationInfoKey merging:dictionaryToMerge];
128}
129
130- (NSDictionary *)registrationSearchQuery
131{
132  return [self registrationSearchQueryMerging:@{}];
133}
134
135- (NSDictionary *)registrationGetQuery
136{
137  return [self registrationSearchQueryMerging:@{
138    (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne,
139    (__bridge id)kSecReturnData:(__bridge id)kCFBooleanTrue
140  }];
141}
142
143- (NSDictionary *)registrationSetQuery:(NSString *)registration
144{
145  return [self registrationSearchQueryMerging:@{
146    (__bridge id)kSecValueData:[registration dataUsingEncoding:NSUTF8StringEncoding],
147    (__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
148  }];
149}
150
151EX_EXPORT_METHOD_AS(getRegistrationInfoAsync,
152                    getRegistrationInfoAsyncWithResolver:(EXPromiseResolveBlock)resolve
153                                                rejecter:(EXPromiseRejectBlock)reject)
154{
155  CFTypeRef keychainResult = NULL;
156  OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)[self registrationGetQuery], &keychainResult);
157  if (status == noErr) {
158    NSData *result = (__bridge_transfer NSData *)keychainResult;
159    NSString *value = [[NSString alloc] initWithData:result
160                                            encoding:NSUTF8StringEncoding];
161    resolve(value);
162  } else if (status == errSecItemNotFound) {
163    resolve(nil);
164  } else {
165    NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
166    reject(@"ERR_NOTIFICATIONS_KEYCHAIN_ACCESS", @"Could not fetch registration information from keychain.", error);
167  }
168}
169
170EX_EXPORT_METHOD_AS(setRegistrationInfoAsync,
171                    setRegistrationInfoAsync:(NSString *)registrationInfo
172                                    resolver:(EXPromiseResolveBlock)resolve
173                                    rejecter:(EXPromiseRejectBlock)reject)
174{
175  // Delete existing registration so we don't need to handle "duplicate item" error
176  SecItemDelete((__bridge CFDictionaryRef)[self registrationSearchQuery]);
177
178  if (registrationInfo) {
179    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)[self registrationSetQuery:registrationInfo], NULL);
180    if (status == errSecSuccess) {
181      resolve(nil);
182    } else {
183      NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
184      reject(@"ERR_NOTIFICATIONS_KEYCHAIN_ACCESS", @"Could not save registration information into keychain.", error);
185    }
186  } else {
187    resolve(nil);
188  }
189}
190
191@end
192