// // HPCredentialsHelper.m // HotPocket // // Created by Tomek Wójcik on 19/09/2025. // #import #import "HPCredentialsHelper.h" @implementation HPCredentials #pragma mark - HPCredentials implementation -(id)init { if (self = [super init]) { self.baseURL = nil; self.accessToken = nil; } return self; } -(BOOL)usable { return (self.baseURL != nil && self.accessToken != nil); } -(NSURL *)rpcURL { return [NSURL URLWithString:self.baseURL]; } -(NSString *)description { NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:2]; if (self.baseURL == nil) { [attributes setValue:@"(null)" forKey:@"baseURL"]; } else { [attributes setValue:self.baseURL forKey:@"baseURL"]; } if (self.accessToken == nil) { [attributes setValue:@"(null)" forKey:@"accessToken"]; } else { [attributes setValue:@"***" forKey:@"accessToken"]; } return [NSString stringWithFormat:@"<%@: %p; %@>", NSStringFromClass([self class]), self, attributes]; } @end @implementation HPCredentialsHelper (HPCredentialsHelperPrivate) #pragma mark - Private interface -(NSString *)getService { #ifdef DEBUG return @"pl.bthlabs.HotPocket.Debug"; #else return @"pl.bthlabs.HotPocket"; #endif } -(NSData *)getKeychainItem:(NSString *)service account:(NSString *)account { if (service == nil || account == nil) { return nil; } NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: service, (__bridge id)kSecAttrAccount: account, (__bridge id)kSecReturnData: @YES, (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, (__bridge id)kSecAttrAccessGroup: @"648728X64K.pl.bthlabs.HotPocketShared", (__bridge id)kSecUseDataProtectionKeychain: @YES, }; CFTypeRef resultData = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &resultData); if (status == errSecSuccess && resultData != NULL) { NSData *result = (__bridge_transfer NSData *)resultData; return result; } else { CFStringRef statusStringRef = SecCopyErrorMessageString(status, NULL); NSString *statusString = (__bridge NSString *)statusStringRef; NSLog(@"-[HPCredentialsHelper getKeychainItem:account:] service=`%@` account=`%@` status=%@", service, account, statusString); return nil; } } -(BOOL)createKeychainItemWithValue:(NSData *)value service:(NSString *)service account:(NSString *)account { if (value == nil || service == nil || account == nil) { return NO; } NSDictionary *attributes = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: service, (__bridge id)kSecAttrAccount: account, (__bridge id)kSecValueData: value, (__bridge id)kSecAttrAccessGroup: @"648728X64K.pl.bthlabs.HotPocketShared", (__bridge id)kSecUseDataProtectionKeychain: @YES, }; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL); if (status != errSecSuccess) { CFStringRef statusStringRef = SecCopyErrorMessageString(status, NULL); NSString *statusString = (__bridge NSString *)statusStringRef; NSLog(@"-[HPCredentialsHelper createKeychainItemWithValue:service:account:] service=`%@` account=`%@` status=%@", service, account, statusString); return NO; } return YES; } -(BOOL)deleteKeychainItem:(NSString *)service account:(NSString *)account { if (service == nil || account == nil) { return NO; } NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService: service, (__bridge id)kSecAttrAccount: account, (__bridge id)kSecAttrAccessGroup: @"648728X64K.pl.bthlabs.HotPocketShared", (__bridge id)kSecUseDataProtectionKeychain: @YES, }; OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); if (status != errSecSuccess) { CFStringRef statusStringRef = SecCopyErrorMessageString(status, NULL); NSString *statusString = (__bridge NSString *)statusStringRef; NSLog(@"-[HPCredentialsHelper deleteKeychainItem:account:] service=`%@` account=`%@` status=%@", service, account, statusString); return NO; } return YES; } @end @implementation HPCredentialsHelper #pragma mark - Initialization +(instancetype)sharedHelper { static HPCredentialsHelper *sharedInstance = nil; static dispatch_once_t initToken; dispatch_once(&initToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } #pragma mark - Public interface -(HPCredentials *)getCredentials { HPCredentials *result = [[HPCredentials alloc] init]; NSData *itemData = [self getKeychainItem:[self getService] account:@"RPC"]; if (itemData != nil) { NSError *error; NSDictionary *itemPayload = [NSJSONSerialization JSONObjectWithData:itemData options:NSJSONReadingTopLevelDictionaryAssumed error:&error]; if (error != nil) { NSLog(@"-[HPCredentialsHalper getCredentials] error=`%@`", error); } else if (itemPayload != nil) { result.baseURL = [itemPayload valueForKey:@"baseURL"]; result.accessToken = [itemPayload valueForKey:@"accessToken"]; } } return result; } -(BOOL)saveCredentials:(NSString *)baseURL accessToken:(NSString *)accessToken { NSMutableDictionary *itemPayload = [NSMutableDictionary dictionaryWithCapacity:2]; if (baseURL != nil) { [itemPayload setValue:baseURL forKey:@"baseURL"]; } if (accessToken != nil) { [itemPayload setValue:accessToken forKey:@"accessToken"]; } NSError *error; NSData *itemData = [NSJSONSerialization dataWithJSONObject:itemPayload options:0 error:&error]; if (error != nil) { NSLog(@"-[HPCredentialsHalper saveCredentials:accessToken:] error=`%@`", error); return NO; } BOOL saveResult = [self createKeychainItemWithValue:itemData service:[self getService] account:@"RPC"]; [[NSNotificationCenter defaultCenter] postNotificationName:@"HPCredentialsChanged" object:self]; return saveResult; } -(BOOL)clearCredentials { BOOL deleteResult = [self deleteKeychainItem:[self getService] account:@"RPC"]; [[NSNotificationCenter defaultCenter] postNotificationName:@"HPCredentialsChanged" object:self]; return deleteResult; } @end