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