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