diff --git a/ios/SSLPinning.mm b/ios/SSLPinning.mm index 8037b88b4e..dab1ed87eb 100644 --- a/ios/SSLPinning.mm +++ b/ios/SSLPinning.mm @@ -14,102 +14,163 @@ #import "SRWebSocket.h" #import "EXSessionTaskDispatcher.h" +static os_log_t SSLLog(void) { + static os_log_t sLog; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sLog = os_log_create("chat.rocket.ssl", "authentication"); + }); + return sLog; +} + @implementation Challenge : NSObject -+(NSURLCredential *)getUrlCredential:(NSURLAuthenticationChallenge *)challenge path:(NSString *)path password:(NSString *)password + ++(NSString *)stringToHex:(NSString *)string { - NSString *authMethod = [[challenge protectionSpace] authenticationMethod]; - SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + char *utf8 = (char *)[string UTF8String]; + NSMutableString *hex = [NSMutableString string]; + while (*utf8) [hex appendFormat:@"%02X", *utf8++ & 0x00FF]; - if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust] || path == nil || password == nil) { - return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; - } else if (path && password) { - NSMutableArray *policies = [NSMutableArray array]; - [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)]; - SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies); + return [[NSString stringWithFormat:@"%@", hex] lowercaseString]; +} - SecTrustResultType result; - SecTrustEvaluate(serverTrust, &result); ++ (NSURLCredential *)getUrlCredential:(NSURLAuthenticationChallenge *)challenge path:(NSString *)path password:(NSString *)password { + os_log_t sslLog = SSLLog(); - if (![[NSFileManager defaultManager] fileExistsAtPath:path]) - { - return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; - } + NSString *authMethod = challenge.protectionSpace.authenticationMethod; + if (![authMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) { + os_log_info(sslLog, "Not a client-certificate challenge"); + return nil; + } - NSData *p12data = [NSData dataWithContentsOfFile:path]; - NSDictionary* options = @{ (id)kSecImportExportPassphrase:password }; - CFArrayRef rawItems = NULL; - OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12data, - (__bridge CFDictionaryRef)options, - &rawItems); + if (path.length == 0 || password.length == 0) { + os_log_info(sslLog, "No path/password configured for client cert"); + return nil; + } - if (status != noErr) { - return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; - } + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + os_log_error(sslLog, "Client cert file not found at path: %{private}@", path); + return nil; + } - NSArray* items = (NSArray*)CFBridgingRelease(rawItems); - NSDictionary* firstItem = nil; - if ((status == errSecSuccess) && ([items count]>0)) { - firstItem = items[0]; - } + NSData *p12data = [NSData dataWithContentsOfFile:path]; + if (!p12data) { + os_log_error(sslLog, "Failed to read PKCS12 data"); + return nil; + } - SecIdentityRef identity = (SecIdentityRef)CFBridgingRetain(firstItem[(id)kSecImportItemIdentity]); - SecCertificateRef certificate = NULL; - if (identity) { - SecIdentityCopyCertificate(identity, &certificate); - if (certificate) { CFRelease(certificate); } - } + NSDictionary *options = @{ (id)kSecImportExportPassphrase : password }; + CFArrayRef rawItems = NULL; + OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12data, + (__bridge CFDictionaryRef)options, + &rawItems); + if (status != errSecSuccess || rawItems == NULL) { + os_log_error(sslLog, "SecPKCS12Import failed: %d", (int)status); + if (rawItems) CFRelease(rawItems); + return nil; + } - NSMutableArray *certificates = [[NSMutableArray alloc] init]; - [certificates addObject:CFBridgingRelease(certificate)]; + NSArray *items = (__bridge_transfer NSArray *)rawItems; + if (items.count == 0) { + os_log_error(sslLog, "PKCS12 import returned zero items"); + return nil; + } - [SDWebImageDownloader sharedDownloader].config.urlCredential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone]; + NSDictionary *firstItem = items[0]; + id identityObj = firstItem[(id)kSecImportItemIdentity]; + if (!identityObj) { + os_log_error(sslLog, "No identity found in PKCS12"); + return nil; + } - return [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone]; + SecIdentityRef identity = (__bridge SecIdentityRef)identityObj; + // Copy certificate from identity + SecCertificateRef certificate = NULL; + OSStatus certStatus = SecIdentityCopyCertificate(identity, &certificate); + if (certStatus != errSecSuccess || certificate == NULL) { + os_log_error(sslLog, "SecIdentityCopyCertificate failed: %d", (int)certStatus); + if (certificate) CFRelease(certificate); + return nil; } - return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; -} + // Build NSArray of certificates (the array should contain certificates, not the identity). + // Some APIs accept an array starting with identity, but NSURLCredential expects + // an array of SecCertificateRef objects (chain). We'll pass the certificate we copied. + id certObj = (__bridge_transfer id)certificate; // certificate will be released by ARC + NSArray *certs = certObj ? @[certObj] : @[]; -+(NSString *)stringToHex:(NSString *)string -{ - char *utf8 = (char *)[string UTF8String]; - NSMutableString *hex = [NSMutableString string]; - while (*utf8) [hex appendFormat:@"%02X", *utf8++ & 0x00FF]; + // Create credential. Choose persistence according to desired policy. + // Use NSURLCredentialPersistenceNone for one-off, or ForSession if you want reuse within a session. + NSURLCredential *cred = [NSURLCredential credentialWithIdentity:identity + certificates:certs + persistence:NSURLCredentialPersistenceNone]; - return [[NSString stringWithFormat:@"%@", hex] lowercaseString]; + os_log_info(sslLog, "Created client credential (certs: %lu)", (unsigned long)certs.count); + return cred; } -+(void)runChallenge:(NSURLSession *)session - didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge - completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler ++ (void)runChallenge:(NSURLSession *)session + didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { - NSString *host = challenge.protectionSpace.host; + os_log_t sslLog = SSLLog(); + static NSInteger seq = 0; seq++; + NSString *host = challenge.protectionSpace.host ?: @"(unknown)"; + NSString *authMethod = challenge.protectionSpace.authenticationMethod ?: @"(none)"; - // Read the clientSSL info from MMKV - __block NSString *clientSSL; - SecureStorage *secureStorage = [[SecureStorage alloc] init]; + os_log_info(sslLog, "Challenge #%ld host=%{public}@ authMethod=%{public}@", (long)seq, host, authMethod); - // https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31 - NSString *key = [secureStorage getSecureKey:[self stringToHex:@"com.MMKV.default"]]; - NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + if ([authMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) { + // Read stored client config (your existing MMKV code) — if not found, perform default handling. + SecureStorage *secureStorage = [[SecureStorage alloc] init]; + NSString *key = [secureStorage getSecureKey:[self stringToHex:@"com.MMKV.default"]]; + if (key == nil) { + os_log_info(sslLog, "No secure storage key -> default handling"); + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } - if (key == NULL) { - return completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential); - } + NSData *cryptKey = [key dataUsingEncoding:NSUTF8StringEncoding]; + MMKV *mmkv = [MMKV mmkvWithID:@"default" cryptKey:cryptKey mode:MMKVMultiProcess]; + NSString *clientSSL = [mmkv getStringForKey:host]; - NSData *cryptKey = [key dataUsingEncoding:NSUTF8StringEncoding]; - MMKV *mmkv = [MMKV mmkvWithID:@"default" cryptKey:cryptKey mode:MMKVMultiProcess]; - clientSSL = [mmkv getStringForKey:host]; + if (!clientSSL) { + os_log_info(sslLog, "No client SSL configuration for host %{public}@", host); + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } - if (clientSSL) { - NSData *data = [clientSSL dataUsingEncoding:NSUTF8StringEncoding]; - id dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - NSString *path = [dict objectForKey:@"path"]; - NSString *password = [dict objectForKey:@"password"]; - credential = [self getUrlCredential:challenge path:path password:password]; + NSData *jsonData = [clientSSL dataUsingEncoding:NSUTF8StringEncoding]; + NSError *jsonErr = nil; + NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonErr]; + if (jsonErr || ![dict isKindOfClass:[NSDictionary class]]) { + os_log_error(sslLog, "Malformed clientSSL JSON for host %{public}@: %{public}@", host, jsonErr.localizedDescription); + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } + + NSString *path = dict[@"path"]; + NSString *password = dict[@"password"]; + if (!path || !password) { + os_log_error(sslLog, "clientSSL entry missing path/password"); + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } + + NSURLCredential *clientCred = [self getUrlCredential:challenge path:path password:password]; + if (clientCred) { + os_log_info(sslLog, "Presenting client certificate for host %{public}@", host); + completionHandler(NSURLSessionAuthChallengeUseCredential, clientCred); + return; + } else { + os_log_error(sslLog, "Failed to build client credential - default handling"); + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); + return; + } } - completionHandler(NSURLSessionAuthChallengeUseCredential, credential); + // For all other auth methods, allow system default handling: + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } @end