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