diff --git a/MessagesBridge.xcodeproj/project.pbxproj b/MessagesBridge.xcodeproj/project.pbxproj index f454c8a..af6d7d7 100644 --- a/MessagesBridge.xcodeproj/project.pbxproj +++ b/MessagesBridge.xcodeproj/project.pbxproj @@ -73,6 +73,8 @@ CD14F1A4219FF22700E7DD22 /* IMMessageItem+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A3219FF22700E7DD22 /* IMMessageItem+Encoded.m */; }; CD14F1AA219FF3B800E7DD22 /* MBIMUpdateQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */; }; CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */; }; + CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */; }; + CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */; }; CD602056219B5DFD0024D9C5 /* MBIMBridgeOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */; }; CD60205C219B623F0024D9C5 /* MBIMMessagesListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */; }; CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205E219B674B0024D9C5 /* MBIMConversationListOperation.m */; }; @@ -217,6 +219,10 @@ CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMUpdateQueue.m; sourceTree = ""; }; CD14F1AB219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMConcurrentHTTPServer.h; sourceTree = ""; }; CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMConcurrentHTTPServer.m; sourceTree = ""; }; + CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthenticateOperation.h; sourceTree = ""; }; + CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthenticateOperation.m; sourceTree = ""; }; + CD2ECEC326953F2A0055E302 /* MBIMAuthToken.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthToken.h; sourceTree = ""; }; + CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthToken.m; sourceTree = ""; }; CD602054219B5DFD0024D9C5 /* MBIMBridgeOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMBridgeOperation.h; sourceTree = ""; }; CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMBridgeOperation.m; sourceTree = ""; }; CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMMessagesListOperation.h; sourceTree = ""; }; @@ -328,13 +334,14 @@ 1A0C4469219A4BC300F2AC00 /* MBIMBridge.h */, 1AAB32AC21F8212E004A2A72 /* MBIMBridge_Private.h */, 1A0C446A219A4BC300F2AC00 /* MBIMBridge.m */, + CD2ECEC326953F2A0055E302 /* MBIMAuthToken.h */, + CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */, CD14F1A8219FF3B800E7DD22 /* MBIMUpdateQueue.h */, CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */, CD14F1AB219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.h */, CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */, 1ACFCFE2219EB45300E2C237 /* MBIMHTTPConnection.h */, 1ACFCFE3219EB45300E2C237 /* MBIMHTTPConnection.m */, - 1AAB32A921F81AD0004A2A72 /* Security */, ); path = Bridge; sourceTree = ""; @@ -352,13 +359,6 @@ path = Utilities; sourceTree = ""; }; - 1AAB32A921F81AD0004A2A72 /* Security */ = { - isa = PBXGroup; - children = ( - ); - path = Security; - sourceTree = ""; - }; 1AAB32AE21F82E73004A2A72 /* Utilities */ = { isa = PBXGroup; children = ( @@ -536,6 +536,8 @@ CD14F1A0219FE7D600E7DD22 /* MBIMUpdatePollOperation.m */, 1AD8936C21EFD986009B599A /* MBIMUploadAttachmentOperation.h */, 1AD8936D21EFD986009B599A /* MBIMUploadAttachmentOperation.m */, + CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */, + CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */, ); path = Operations; sourceTree = ""; @@ -831,6 +833,7 @@ CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */, 1AA43E95219EC38E00EDF1A7 /* MBIMHTTPUtilities.m in Sources */, CDE455A421A5308D0041F5DD /* MBIMFetchAttachmentOperation.m in Sources */, + CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */, CD83E156219BE10A00F4CCEA /* hooking.m in Sources */, 1AAB32B121F82EB7004A2A72 /* MBIMLogging.m in Sources */, 1AD8936E21EFD986009B599A /* MBIMUploadAttachmentOperation.m in Sources */, @@ -847,6 +850,7 @@ CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */, CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */, 1AA43E8F219EBB2D00EDF1A7 /* MBIMJSONDataResponse.m in Sources */, + CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/kordophone/Bridge/MBIMAuthToken.h b/kordophone/Bridge/MBIMAuthToken.h new file mode 100644 index 0000000..93da579 --- /dev/null +++ b/kordophone/Bridge/MBIMAuthToken.h @@ -0,0 +1,26 @@ +// +// MBIMAuthToken.h +// MBIMAuthToken +// +// Created by James Magahern on 7/6/21. +// Copyright © 2021 James Magahern. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MBIMAuthToken : NSObject +@property (nonatomic, readonly) NSString *username; +@property (nonatomic, readonly) NSString *jwtToken; +@property (nonatomic, readonly) NSDate *expirationDate; + +- (instancetype)initWithUsername:(NSString *)username NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithTokenString:(NSString *)tokenString NS_DESIGNATED_INITIALIZER; + +- (BOOL)isValid; + +- (instancetype)init NS_UNAVAILABLE; +@end + +NS_ASSUME_NONNULL_END diff --git a/kordophone/Bridge/MBIMAuthToken.m b/kordophone/Bridge/MBIMAuthToken.m new file mode 100644 index 0000000..8ce5c7b --- /dev/null +++ b/kordophone/Bridge/MBIMAuthToken.m @@ -0,0 +1,149 @@ +// +// MBIMAuthToken.m +// MBIMAuthToken +// +// Created by James Magahern on 7/6/21. +// Copyright © 2021 James Magahern. All rights reserved. +// + +#import "MBIMAuthToken.h" +#import + +#define HOUR 3600 +#define DAY (24*HOUR) + +static const NSTimeInterval ExpirationTime = 15 * DAY; +static const char *SecretKey = "709E7CD8-4983-4D5F-B7BF-8B1C6341D2DB"; + +static NSString *const kUsernamePayloadKey = @"user"; +static NSString *const kIssuerPayloadKey = @"iss"; +static NSString *const kExpirationDatePayloadKey = @"exp"; + +static NSString *const kIssuerPayloadValue = @"kordophone"; + +@interface MBIMAuthToken () +@property (nonatomic, copy) NSString *username; +@property (nonatomic, copy) NSString *jwtToken; +@property (nonatomic, copy) NSDate *expirationDate; + +// JWT Payload data +@property (nonatomic, copy) NSString *headerString; +@property (nonatomic, copy) NSString *payloadString; +@property (nonatomic, copy) NSData *signatureData; +@end + +@implementation MBIMAuthToken + +- (instancetype)initWithUsername:(NSString *)username +{ + self = [super init]; + if (self) { + self.username = username; + self.expirationDate = [NSDate dateWithTimeIntervalSinceNow:ExpirationTime]; + } + + return self; +} + +- (instancetype)initWithTokenString:(NSString *)tokenString +{ + NSArray *components = [tokenString componentsSeparatedByString:@"."]; + if (components.count != 3) { + return nil; + } + + NSString *header = components[0]; + NSString *payload = components[1]; + NSString *signature = components[2]; + + NSData *payloadData = [[NSData alloc] initWithBase64EncodedString:payload options:0]; + NSDictionary *decodedPayload = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil]; + if (!decodedPayload) { + return nil; + } + + if (![decodedPayload[kIssuerPayloadKey] isEqualToString:@"kordophone"]) { + return nil; + } + + self = [super init]; + if (self) { + _headerString = header; + _payloadString = payload; + _signatureData = [[NSData alloc] initWithBase64EncodedString:signature options:0]; + + _username = decodedPayload[kUsernamePayloadKey]; + + NSTimeInterval expirationDate = [decodedPayload[kExpirationDatePayloadKey] floatValue]; + _expirationDate = [NSDate dateWithTimeIntervalSince1970:expirationDate]; + } + + return self; +} + +- (NSUInteger)hash +{ + return (_username.hash ^ _expirationDate.hash); +} + +- (NSString *)jwtToken +{ + if (!_jwtToken) { + NSDictionary *header = @{ + @"alg" : @"HS256", + @"typ" : @"jwt" + }; + + NSData *headerData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil]; + NSString *headerStr = [headerData base64EncodedStringWithOptions:0]; + + NSInteger expirationDate = [_expirationDate timeIntervalSince1970]; + NSDictionary *payload = @{ + kUsernamePayloadKey : _username, + kIssuerPayloadKey : kIssuerPayloadValue, + kExpirationDatePayloadKey : [NSString stringWithFormat:@"%ld", expirationDate] + }; + NSData *payloadData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil]; + NSString *payloadStr = [payloadData base64EncodedStringWithOptions:0]; + + NSString *jwtDataStr = [NSString stringWithFormat:@"%@.%@", headerStr, payloadStr]; + NSData *jwtData = [jwtDataStr dataUsingEncoding:NSASCIIStringEncoding]; + + unsigned char signature[CC_SHA256_DIGEST_LENGTH]; + CCHmac(kCCHmacAlgSHA256, SecretKey, sizeof(SecretKey), jwtData.bytes, jwtData.length, signature); + + NSData *signatureData = [NSData dataWithBytes:signature length:CC_SHA256_DIGEST_LENGTH]; + NSString *signatureStr = [signatureData base64EncodedStringWithOptions:0]; + + _jwtToken = [NSString stringWithFormat:@"%@.%@", jwtDataStr, signatureStr]; + } + + return _jwtToken; +} + +- (BOOL)isValid +{ + // Verify expiration date + BOOL expirationDateValid = [_expirationDate timeIntervalSinceNow] > 0; + if (!expirationDateValid) { + MBIMLogInfo(@"Auth token expired."); + return NO; + } + + // Verify signature + NSString *verificationDataStr = [NSString stringWithFormat:@"%@.%@", _headerString, _payloadString]; + NSData *verificationData = [verificationDataStr dataUsingEncoding:NSASCIIStringEncoding]; + + unsigned char computedSignature[CC_SHA256_DIGEST_LENGTH]; + CCHmac(kCCHmacAlgSHA256, SecretKey, sizeof(SecretKey), verificationData.bytes, verificationData.length, computedSignature); + + NSData *computedSignatureData = [NSData dataWithBytes:computedSignature length:CC_SHA256_DIGEST_LENGTH]; + if (![computedSignatureData isEqualToData:_signatureData]) { + MBIMLogInfo(@"Auth token signature verification failed."); + return NO; + } + + return YES; +} + +@end diff --git a/kordophone/Bridge/MBIMHTTPConnection.m b/kordophone/Bridge/MBIMHTTPConnection.m index 5f3ef9a..2cbea07 100644 --- a/kordophone/Bridge/MBIMHTTPConnection.m +++ b/kordophone/Bridge/MBIMHTTPConnection.m @@ -11,8 +11,14 @@ #import "MBIMBridge.h" #import "MBIMBridge_Private.h" #import "MBIMBridgeOperation.h" +#import "MBIMAuthToken.h" #import +#import + +@interface HTTPConnection (/* INTERNAL */) +- (BOOL)isAuthenticated; +@end @implementation MBIMHTTPConnection { NSMutableData *_bodyData; @@ -31,7 +37,15 @@ - (BOOL)isPasswordProtected:(NSString *)path { - return [[MBIMBridge sharedInstance] usesAccessControl]; + if ([[MBIMBridge sharedInstance] usesAccessControl]) { + NSURL *url = [NSURL URLWithString:path]; + NSString *endpointName = [url lastPathComponent]; + + Class operationClass = [MBIMBridgeOperation operationClassForEndpointName:endpointName]; + return [operationClass requiresAuthentication]; + } + + return NO; } - (NSString *)passwordForUser:(NSString *)username @@ -41,7 +55,23 @@ return bridge.authPassword; } - return @""; + // nil means "user not in system" + return nil; +} + +- (BOOL)isAuthenticated +{ + NSString *authInfo = [request headerField:@"Authorization"]; + if ([authInfo hasPrefix:@"Bearer"]) { + NSArray *bearerAuthTuple = [authInfo componentsSeparatedByString:@" "]; + if ([bearerAuthTuple count] == 2) { + NSString *jwtToken = [bearerAuthTuple objectAtIndex:1]; + MBIMAuthToken *authToken = [[MBIMAuthToken alloc] initWithTokenString:jwtToken]; + return [authToken isValid]; + } + } + + return [super isAuthenticated]; } - (BOOL)useDigestAccessAuthentication diff --git a/kordophone/Bridge/Operations/MBIMAuthenticateOperation.h b/kordophone/Bridge/Operations/MBIMAuthenticateOperation.h new file mode 100644 index 0000000..8709e91 --- /dev/null +++ b/kordophone/Bridge/Operations/MBIMAuthenticateOperation.h @@ -0,0 +1,17 @@ +// +// MBIMAuthenticateOperation.h +// MBIMAuthenticateOperation +// +// Created by James Magahern on 7/6/21. +// Copyright © 2021 James Magahern. All rights reserved. +// + +#import "MBIMBridgeOperation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MBIMAuthenticateOperation : MBIMBridgeOperation + +@end + +NS_ASSUME_NONNULL_END diff --git a/kordophone/Bridge/Operations/MBIMAuthenticateOperation.m b/kordophone/Bridge/Operations/MBIMAuthenticateOperation.m new file mode 100644 index 0000000..0988a4f --- /dev/null +++ b/kordophone/Bridge/Operations/MBIMAuthenticateOperation.m @@ -0,0 +1,72 @@ +// +// MBIMAuthenticateOperation.m +// MBIMAuthenticateOperation +// +// Created by James Magahern on 7/6/21. +// Copyright © 2021 James Magahern. All rights reserved. +// + +#import "MBIMAuthenticateOperation.h" +#import "MBIMBridge.h" +#import "MBIMAuthToken.h" + +@implementation MBIMAuthenticateOperation + ++ (void)load { [super load]; } + ++ (NSString *)endpointName +{ + return @"authenticate"; +} + ++ (BOOL)requiresAuthentication +{ + return NO; +} + +- (void)main +{ + NSObject *response = nil; + + if (self.requestBodyData.length == 0) { + self.serverCompletionBlock([[HTTPErrorResponse alloc] initWithErrorCode:400]); + return; + } + + NSError *error = nil; + NSDictionary *args = [NSJSONSerialization JSONObjectWithData:self.requestBodyData options:0 error:&error]; + if (error || args.count == 0) { + response = [[HTTPErrorResponse alloc] initWithErrorCode:400]; + } else { + do { + NSString *username = [args objectForKey:@"username"]; + NSString *password = [args objectForKey:@"password"]; + + if (!username || !password) { + response = [[HTTPErrorResponse alloc] initWithErrorCode:400]; + break; + } + + if (![MBIMBridge.sharedInstance.authUsername isEqualToString:username]) { + response = [[HTTPErrorResponse alloc] initWithErrorCode:401]; + break; + } + + if (![MBIMBridge.sharedInstance.authPassword isEqualToString:password]) { + response = [[HTTPErrorResponse alloc] initWithErrorCode:401]; + break; + } + + MBIMAuthToken *token = [[MBIMAuthToken alloc] initWithUsername:username]; + + // All systems go + response = [MBIMJSONDataResponse responseWithJSONObject:@{ + @"jwt" : token.jwtToken + }]; + } while (NO); + } + + self.serverCompletionBlock(response); +} + +@end diff --git a/kordophone/Bridge/Operations/MBIMBridgeOperation.h b/kordophone/Bridge/Operations/MBIMBridgeOperation.h index b07d1bc..936050c 100644 --- a/kordophone/Bridge/Operations/MBIMBridgeOperation.h +++ b/kordophone/Bridge/Operations/MBIMBridgeOperation.h @@ -18,6 +18,7 @@ typedef void (^MBIMBridgeOperationCompletionBlock)(NSObject * _Nul @interface MBIMBridgeOperation : NSOperation @property (class, nonatomic, readonly) NSString *endpointName; +@property (class, nonatomic, readonly) BOOL requiresAuthentication; // default YES @property (nonatomic, strong) NSData *requestBodyData; @property (nonatomic, readonly) NSURL *requestURL; diff --git a/kordophone/Bridge/Operations/MBIMBridgeOperation.m b/kordophone/Bridge/Operations/MBIMBridgeOperation.m index 5e1ae26..a64c632 100644 --- a/kordophone/Bridge/Operations/MBIMBridgeOperation.m +++ b/kordophone/Bridge/Operations/MBIMBridgeOperation.m @@ -55,6 +55,11 @@ return [[self _operationClassMapping] objectForKey:endpointName]; } ++ (BOOL)requiresAuthentication +{ + return YES; +} + - (instancetype)initWithRequestURL:(NSURL *)requestURL completion:(MBIMBridgeOperationCompletionBlock)completionBlock { self = [super init];