diff --git a/.github/workflows/stable.build-push.yml b/.github/workflows/stable.build-push.yml index 8960e61d57..0a2e31c9e4 100644 --- a/.github/workflows/stable.build-push.yml +++ b/.github/workflows/stable.build-push.yml @@ -71,8 +71,8 @@ jobs: run: ./scripts/uploadNonAlpha.sh stable - name: Publish catalyst to appstore connect run: xcrun altool --upload-app --file ./Monal/build/app/Monal.pkg --type macos --asc-provider S8D843U34Y -u "$(cat /Users/ci/apple_connect_upload_mail.txt)" -p "$(cat /Users/ci/apple_connect_upload_secret.txt)" --primary-bundle-id maccatalyst.G7YU7X7KRJ.SworIM - - name: Update xmpp.org client list with new timestamp - run: ./scripts/push_xmpp.org.sh + # - name: Update xmpp.org client list with new timestamp + # run: ./scripts/push_xmpp.org.sh - uses: actions/upload-artifact@v2 with: name: monal-catalyst-pkg diff --git a/.gitignore b/.gitignore index 4621212b16..5216ab09dc 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock +!rust/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Monal/Classes/AVCallUI.swift b/Monal/Classes/AVCallUI.swift index a28a860b68..e30eef7759 100644 --- a/Monal/Classes/AVCallUI.swift +++ b/Monal/Classes/AVCallUI.swift @@ -29,6 +29,8 @@ struct VideoView: UIViewRepresentable { struct AVCallUI: View { @StateObject private var call: ObservableKVOWrapper @StateObject private var contact: ObservableKVOWrapper + @State private var showMicAlert = false + @State private var showSecurityHelpAlert: MLCallEncryptionState? = nil private var ringingPlayer: AVAudioPlayer! private var busyPlayer: AVAudioPlayer! private var errorPlayer: AVAudioPlayer! @@ -84,41 +86,84 @@ struct AVCallUI: View { Group { Spacer().frame(height: 24) - HStack { - switch MLCallDirection(rawValue:call.direction) { - case .incoming: - Image(systemName: "phone.arrow.down.left") - .resizable() - .frame(width: 20.0, height: 20.0) - .foregroundColor(.primary) - case .outgoing: - Image(systemName: "phone.arrow.up.right") - .resizable() - .frame(width: 20.0, height: 20.0) - .foregroundColor(.primary) - default: //should never be reached - Text("") + HStack(alignment: .top) { + Spacer().frame(width:20) + + VStack { + Spacer().frame(height: 8) + switch MLCallDirection(rawValue:call.direction) { + case .incoming: + Image(systemName: "phone.arrow.down.left") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.primary) + case .outgoing: + Image(systemName: "phone.arrow.up.right") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.primary) + default: //should never be reached + Text("") + } + } + + VStack { + Spacer().frame(height: 8) + Button(action: { + //show dialog explaining different encryption states + self.showSecurityHelpAlert = MLCallEncryptionState(rawValue:call.encryptionState) + }, label: { + switch MLCallEncryptionState(rawValue:call.encryptionState) { + case .unknown: + Text("") + case .clear: + Spacer().frame(width: 10) + Image(systemName: "xmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.red) + case .toFU: + Spacer().frame(width: 10) + Image(systemName: "checkmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.yellow) + case .trusted: + Spacer().frame(width: 10) + Image(systemName: "checkmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.green) + default: //should never be reached + Text("") + } + }) } - Spacer().frame(width: 20) + Spacer() Text(contact.contactDisplayName as String) .font(.largeTitle) .foregroundColor(.primary) - Spacer().frame(width: 20) + Spacer() - Button(action: { - self.delegate.dismissWithoutAnimation() - if let activeChats = self.appDelegate.activeChats { - activeChats.presentChat(with:self.contact.obj) - } - }, label: { - Image(systemName: "text.bubble") - .resizable() - .frame(width: 28.0, height: 28.0) - .foregroundColor(.primary) - }) + VStack { + Spacer().frame(height: 8) + Button(action: { + self.delegate.dismissWithoutAnimation() + if let activeChats = self.appDelegate.activeChats { + activeChats.presentChat(with:self.contact.obj) + } + }, label: { + Image(systemName: "text.bubble") + .resizable() + .frame(width: 28.0, height: 28.0) + .foregroundColor(.primary) + }) + } + + Spacer().frame(width:20) } Spacer().frame(height: 16) @@ -160,6 +205,10 @@ struct AVCallUI: View { Text("Call ended: connection failed") .bold() .foregroundColor(.primary) + case .securityError: + Text("Call ended: couldn't establish encryption") + .bold() + .foregroundColor(.primary) case .unanswered: Text("Call was not answered") .bold() @@ -365,6 +414,49 @@ struct AVCallUI: View { Spacer().frame(height: 32) } } + .alert(isPresented: $showMicAlert) { + Alert( + title: Text("Missing permission"), + message: Text("You need to grant microphone access in iOS Settings-> Privacy-> Microphone, if you want that others can hear you."), + dismissButton: .default(Text("OK")) + ) + } + .richAlert(isPresented:$showSecurityHelpAlert, title:Text("Call security help").foregroundColor(.black)) { + VStack(alignment: .leading) { + HStack { + Image(systemName: "xmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.red) + Spacer().frame(width: 10) + Text("Red x-mark shield:") + }.font(Font.body.weight(showSecurityHelpAlert == .clear ? .heavy : .medium)) + Text("This means your call is encrypted, but the remote party could not be verified using OMEMO encryption.\nYour or the callee's XMPP server could possibly Man-In-The-Middle you.") + Spacer().frame(height: 20) + + HStack { + Image(systemName: "checkmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.yellow) + Spacer().frame(width: 10) + Text("Yellow checkmark shield:") + }.font(Font.body.weight(showSecurityHelpAlert == .toFU ? .heavy : .medium)) + Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nBut since you did not manually verify the callee's OMEMO fingerprints, your or the callee's XMPP server could possibly have inserted their own OMEMO keys to Man-In-The-Middle you.") + Spacer().frame(height: 20) + + HStack { + Image(systemName: "checkmark.shield.fill") + .resizable() + .frame(width: 20.0, height: 20.0) + .foregroundColor(.green) + Spacer().frame(width: 10) + Text("Green checkmark shield:") + }.font(Font.body.weight(showSecurityHelpAlert == .trusted ? .heavy : .medium)) + Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nYou manually verified the used OMEMO keys and no Man-In-The-Middle can take place.") + Spacer().frame(height: 20) + }.foregroundColor(.black) + } .onAppear { //force portrait mode and lock ui there UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") @@ -372,6 +464,13 @@ struct AVCallUI: View { self.ringingPlayer.numberOfLoops = -1 self.busyPlayer.numberOfLoops = -1 self.errorPlayer.numberOfLoops = -1 + + //ask for mic permissions + AVAudioSession.sharedInstance().requestRecordPermission { granted in + if !granted { + showMicAlert = true + } + } } .onDisappear { //allow all orientations again @@ -408,6 +507,11 @@ struct AVCallUI: View { ringingPlayer.stop() busyPlayer.stop() errorPlayer.play() + case .securityError: + DDLogDebug("state: finished: securityError") + ringingPlayer.stop() + busyPlayer.stop() + errorPlayer.play() case .unanswered: DDLogDebug("state: finished: unanswered") ringingPlayer.stop() diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index b203d67b08..15e7daac85 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -27,7 +27,6 @@ NS_ASSUME_NONNULL_BEGIN @class xmpp; @class XMPPStanza; @class UNNotificationRequest; -@class DDLogFormatter; @class DDLogMessage; @class MLFileLogger; @class UIView; @@ -40,7 +39,7 @@ void swizzle(Class c, SEL orig, SEL new); @property (class, nonatomic, strong) MLFileLogger* fileLogger; -+(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage usingFormatter:(id _Nullable) formatter counter:(uint64_t*) counter andError:(NSError** _Nullable) error; ++(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage counter:(uint64_t*) counter andError:(NSError** _Nullable) error; +(void) initSystem; +(void) installExceptionHandler; +(int) pendingCrashreportCount; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index 9e3cf7e49d..92408a782e 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -15,6 +15,7 @@ #include #include #include +#import #import #import #import @@ -168,8 +169,9 @@ static void addFilePathWithSize(const KSCrashReportWriter* writer, char* name, c static void crash_callback(const KSCrashReportWriter* writer) { - asyncSafeCopyFile(_origLogfilePath, _logfilePath); + int copyRetval = asyncSafeCopyFile(_origLogfilePath, _logfilePath); writer->addStringElement(writer, "logfileCopied", "YES"); + writer->addIntegerElement(writer, "logfileCopyResult", copyRetval); addFilePathWithSize(writer, "logfileCopy", _logfilePath); //this comes last to make sure we see size differences if the logfile got written during crash data collection (could be other processes) addFilePathWithSize(writer, "currentLogfile", _origLogfilePath); @@ -1373,11 +1375,10 @@ +(void) activityLog if(log_activity) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ - BOOL appex = [HelperTools isAppExtension]; unsigned long counter = 1; while(counter++) { - DDLogInfo(@"activity(%@): %lu, memory used / available: %.3fMiB / %.3fMiB", appex ? @"APPEX" : @"MAINAPP", counter, [self report_memory], (CGFloat)os_proc_available_memory() / 1048576); + DDLogInfo(@"activity: %lu, memory used / available: %.3fMiB / %.3fMiB", counter, [self report_memory], (CGFloat)os_proc_available_memory() / 1048576); [NSThread sleepForTimeInterval:1]; } }); @@ -1404,26 +1405,42 @@ +(void) setFileLogger:(DDFileLogger*) fileLogger _fileLogger = fileLogger; } -+(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage usingFormatter:(id _Nullable) formatter counter:(uint64_t*) counter andError:(NSError** _Nullable) error ++(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage counter:(uint64_t*) counter andError:(NSError** _Nullable) error { - //format message using given formatter - NSString* logMsg = logMessage.message; - NSString* timestamp = [[NSISO8601DateFormatter new] stringFromDate:logMessage.timestamp]; - if(formatter) - { - logMsg = [NSString stringWithFormat:@"%@", [formatter formatLogMessage:logMessage]]; - timestamp = [(MLLogFormatter*)formatter stringFromDate:logMessage.timestamp]; - } + static NSDateFormatter* dateFormatter = nil; + static NSString* (^qos2name)(NSUInteger) = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss:SSS"]; + [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; + [dateFormatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]]; + + qos2name = ^(NSUInteger qos) { + switch ((qos_class_t) qos) { + case QOS_CLASS_USER_INTERACTIVE: return @"QOS_CLASS_USER_INTERACTIVE"; + case QOS_CLASS_USER_INITIATED: return @"QOS_CLASS_USER_INITIATED"; + case QOS_CLASS_DEFAULT: return @"QOS_CLASS_DEFAULT"; + case QOS_CLASS_UTILITY: return @"QOS_CLASS_UTILITY"; + case QOS_CLASS_BACKGROUND: return @"QOS_CLASS_BACKGROUND"; + default: return [NSString stringWithFormat:@"QOS_UNKNOWN(%lu)", (unsigned long)qos]; + } + }; + }); //construct json dictionary (*counter)++; NSDictionary* representedObject = @{ @"queueThreadLabel": [self getQueueThreadLabelFor:logMessage], @"processType": [self isAppExtension] ? @"appex" : @"mainapp", - @"representedObject": logMessage.representedObject ? logMessage.representedObject : [NSNull null] + @"processName": [[[NSBundle mainBundle] executablePath] lastPathComponent], + @"counter": [NSNumber numberWithUnsignedLongLong:*counter], + @"processID": _processID, + @"qosName": qos2name(logMessage.qos), + @"representedObject": logMessage.representedObject ? logMessage.representedObject : [NSNull null], }; NSDictionary* msgDict = @{ - @"formattedMessage": logMsg, @"messageFormat": logMessage.messageFormat, @"message": logMessage.message, @"level": [NSNumber numberWithInteger:logMessage.level], @@ -1435,13 +1452,11 @@ +(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage usin @"line": [NSNumber numberWithInteger:logMessage.line], @"tag": representedObject, @"options": [NSNumber numberWithInteger:logMessage.options], - @"timestamp": timestamp, + @"timestamp": [dateFormatter stringFromDate:logMessage.timestamp], @"threadID": logMessage.threadID, @"threadName": logMessage.threadName, @"queueLabel": logMessage.queueLabel, @"qos": [NSNumber numberWithInteger:logMessage.qos], - @"_counter": [NSNumber numberWithUnsignedLongLong:*counter], - @"_processID": _processID, }; //encode json into NSData @@ -1466,22 +1481,16 @@ +(void) flushLogsWithTimeout:(double) timeout +(void) configureLogging { - //create log formatter - MLLogFormatter* formatter = [MLLogFormatter new]; - //don't log to the console (aka stderr) to not create loops with our redirected stderr // //start console logger first (this one will *not* log own additional (and duplicated) informations like DDOSLogger would) // #if TARGET_OS_SIMULATOR -// [[DDTTYLogger sharedInstance] setLogFormatter:formatter]; // [DDLog addLogger:[DDTTYLogger sharedInstance]]; // #else -// [[DDOSLogger sharedInstance] setLogFormatter:formatter]; // [DDLog addLogger:[DDOSLogger sharedInstance]]; // #endif //network logger (start as early as possible) MLUDPLogger* udpLogger = [MLUDPLogger new]; - [udpLogger setLogFormatter:formatter]; [DDLog addLogger:udpLogger]; //redirect stderr containing NSLog() messages @@ -1501,8 +1510,7 @@ +(void) configureLogging self.fileLogger = [[MLFileLogger alloc] initWithLogFileManager:logFileManager]; self.fileLogger.doNotReuseLogFiles = NO; self.fileLogger.rollingFrequency = 60 * 60 * 48; // 48 hour rolling - self.fileLogger.maximumFileSize = 256 * 1024 * 1024; - self.fileLogger.logFormatter = formatter; + self.fileLogger.maximumFileSize = 128 * 1024 * 1024; self.fileLogger.archiveAllowed = YES; //everything is configured now, engage logfile archiving [DDLog addLogger:self.fileLogger]; @@ -1523,6 +1531,12 @@ +(void) configureLogging DDLogInfo(@"Starting: Version %@ (%@ %@ UTC, %@)", version, buildDate, buildTime, [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]); [DDLog flushLog]; + DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_USER_INTERACTIVE", QOS_CLASS_USER_INTERACTIVE); + DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_USER_INITIATED", QOS_CLASS_USER_INITIATED); + DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_DEFAULT", QOS_CLASS_DEFAULT); + DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_UTILITY", QOS_CLASS_UTILITY); + DDLogVerbose(@"QOS level: %@ = %d", @"QOS_CLASS_BACKGROUND", QOS_CLASS_BACKGROUND); + //remove old ascii based logfiles for(NSString* file in [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:containerUrl error:nil] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self LIKE %@", @"Monal *.log"]]) { @@ -1609,6 +1623,7 @@ +(void) installCrashHandler NSString* buildTime = [NSString stringWithUTF8String:__TIME__]; handler.userInfo = @{ @"isAppex": @([self isAppExtension]), + @"processName": [[[NSBundle mainBundle] executablePath] lastPathComponent], @"bundleName": nilWrapper([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]), @"appVersion": [NSString stringWithFormat:NSLocalizedString(@"Version %@ (%@ %@ UTC)", @""), version, buildDate, buildTime], }; diff --git a/Monal/Classes/MLCall.h b/Monal/Classes/MLCall.h index 3b5505b110..69b5d0c4b6 100644 --- a/Monal/Classes/MLCall.h +++ b/Monal/Classes/MLCall.h @@ -44,6 +44,7 @@ typedef NS_ENUM(NSUInteger, MLCallFinishReason) { MLCallFinishReasonUnknown, //dummy default value MLCallFinishReasonNormal, //used for a call answered and finished locally (call direction etc. don't matter here) MLCallFinishReasonConnectivityError, //used for a call accepted but not connected (call direction etc. don't matter here) + MLCallFinishReasonSecurityError, //used for a call that could not be encrypted using OMEMO MLCallFinishReasonUnanswered, //used for a call retracted remotely (always remote party) MLCallFinishReasonAnsweredElsewhere, //used for a call answered and finished remotely (own account OR remote party) MLCallFinishReasonRetracted, //used for a call retracted locally (always own acount) @@ -52,6 +53,13 @@ typedef NS_ENUM(NSUInteger, MLCallFinishReason) { MLCallFinishReasonError, //used for a call error }; +typedef NS_ENUM(NSUInteger, MLCallEncryptionState) { + MLCallEncryptionStateUnknown, + MLCallEncryptionStateClear, + MLCallEncryptionStateToFU, + MLCallEncryptionStateTrusted, +}; + @interface MLCall : NSObject @property (strong, readonly) NSString* description; @@ -60,6 +68,7 @@ typedef NS_ENUM(NSUInteger, MLCallFinishReason) { @property (nonatomic, strong, readonly) MLContact* contact; @property (nonatomic, readonly) MLCallType callType; @property (nonatomic, readonly) MLCallDirection direction; +@property (nonatomic, readonly) MLCallEncryptionState encryptionState; @property (nonatomic, readonly) MLCallState state; @property (nonatomic, readonly) MLCallFinishReason finishReason; @property (nonatomic, readonly) uint32_t durationTime; diff --git a/Monal/Classes/MLCall.m b/Monal/Classes/MLCall.m index 49ac4cc9f7..019c764d78 100644 --- a/Monal/Classes/MLCall.m +++ b/Monal/Classes/MLCall.m @@ -17,6 +17,7 @@ #import "MLVoIPProcessor.h" #import "MLCall.h" #import "MonalAppDelegate.h" +#import "MLOMEMO.h" @import CallKit; @import WebRTC; @@ -38,6 +39,7 @@ @interface MLCall() @property (nonatomic, strong) MLContact* contact; @property (nonatomic) MLCallType callType; @property (nonatomic) MLCallDirection direction; +@property (nonatomic) MLCallEncryptionState encryptionState; @property (nonatomic, strong) MLXMLNode* _Nullable jmiPropose; @property (nonatomic, strong) MLXMLNode* _Nullable jmiProceed; @@ -59,7 +61,10 @@ @interface MLCall() @property (nonatomic, strong) monal_void_block_t _Nullable cancelWaitUntilIceRestart; @property (nonatomic, strong) MLXMLNode* localSDP; @property (nonatomic, strong) MLXMLNode* remoteSDP; +@property (nonatomic, strong) NSNumber* remoteOmemoDeviceId; @property (nonatomic, strong) NSMutableArray* candidateQueue; +@property (nonatomic, assign) BOOL isEncrypted; + @property (nonatomic, readonly) xmpp* account; @property (nonatomic, strong) MLVoIPProcessor* voipProcessor; @@ -92,6 +97,7 @@ -(instancetype) initWithUUID:(NSUUID*) uuid jmiid:(NSString*) jmiid contact:(MLC self.contact = contact; self.callType = callType; self.direction = direction; + self.encryptionState = MLCallEncryptionStateUnknown; self.isConnected = NO; self.wasConnectedOnce = NO; self.isReconnecting = NO; @@ -103,6 +109,7 @@ -(instancetype) initWithUUID:(NSUUID*) uuid jmiid:(NSString*) jmiid contact:(MLC self.cancelConnectingTimeout = nil; self.localSDP = nil; self.remoteSDP = nil; + self.remoteOmemoDeviceId = nil; self.candidateQueue = [NSMutableArray new]; [HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{ @@ -119,9 +126,9 @@ -(instancetype) initWithUUID:(NSUUID*) uuid jmiid:(NSString*) jmiid contact:(MLC return self; } --(void) deinit +-(void) dealloc { - DDLogInfo(@"Call deinit: %@", self); + DDLogInfo(@"Called dealloc: %@", self); [self.callDurationTimer invalidate]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -290,6 +297,12 @@ -(void) setJmiProceed:(MLXMLNode*) jmiProceed { @synchronized(self) { _jmiProceed = jmiProceed; + if(self.direction == MLCallDirectionOutgoing) + { + //see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd + self.remoteOmemoDeviceId = [jmiProceed findFirst:@"{urn:xmpp:jingle-message:0}proceed/{http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification}device@id|uint"]; + DDLogInfo(@"Proceed set remote omemo deviceid to: %@", self.remoteOmemoDeviceId); + } if(self.direction == MLCallDirectionOutgoing && self.webRTCClient != nil) [self establishOutgoingConnection]; } @@ -392,6 +405,15 @@ -(void) didActivateAudioSession:(AVAudioSession*) audioSession { DDLogInfo(@"Activating audio session now: %@", audioSession); [[RTCAudioSession sharedInstance] lockForConfiguration]; + NSUInteger options = 0; + options |= AVAudioSessionCategoryOptionAllowBluetooth; + options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP; + options |= AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers; + options |= AVAudioSessionCategoryOptionAllowAirPlay; + NSError* error = nil; + [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:options error:&error]; + if(error != nil) + DDLogError(@"Failed to configure AVAudioSession: %@", error); [[RTCAudioSession sharedInstance] audioSessionDidActivate:audioSession]; [[RTCAudioSession sharedInstance] setIsAudioEnabled:YES]; [[RTCAudioSession sharedInstance] unlockForConfiguration]; @@ -442,6 +464,11 @@ -(void) migrateTo:(MLCall*) otherCall self.jmiProceed = nil; [self.callDurationTimer invalidate]; self.callDurationTimer = nil; + self.localSDP = otherCall.localSDP; //should be nil + self.remoteSDP = otherCall.remoteSDP; //should be nil + self.candidateQueue = otherCall.candidateQueue; //should be empty + self.remoteOmemoDeviceId = otherCall.remoteOmemoDeviceId; //depends on jmiProceed and should be empty + self.encryptionState = MLCallEncryptionStateUnknown; //depends on callstate >= connecting otherCall = nil; DDLogDebug(@"%@: Stopping all running timers...", [self short]); @@ -567,6 +594,11 @@ -(void) internalUpdateCallKitState [self sendJmiFinishWithReason:@"connectivity-error"]; [self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed]; } + else if(self.finishReason == MLCallFinishReasonSecurityError) + { + [self sendJmiFinishWithReason:@"security-error"]; + [self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed]; + } else if(self.finishReason == MLCallFinishReasonError) [self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed]; else @@ -608,6 +640,11 @@ -(void) internalUpdateCallKitState [self sendJmiFinishWithReason:@"connectivity-error"]; [self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed]; } + else if(self.finishReason == MLCallFinishReasonSecurityError) + { + [self sendJmiFinishWithReason:@"security-error"]; + [self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed]; + } else if(self.finishReason == MLCallFinishReasonError) [self.voipProcessor.cxProvider reportCallWithUUID:self.uuid endedAtDate:nil reason:CXCallEndedReasonFailed]; else @@ -740,16 +777,24 @@ -(void) handleConnectivityChange:(NSNotification*) notification -(void) offerSDP { + //see https://webrtc.googlesource.com/src/+/refs/heads/main/sdk/objc/api/peerconnection/RTCSessionDescription.h [self.webRTCClient offerWithCompletion:^(RTCSessionDescription* sdp) { - DDLogDebug(@"WebRTC reported local SDP '%@' offer, sending to '%@': %@", [RTCSessionDescription stringForType:sdp.type], self.fullRemoteJid, sdp.sdp); + DDLogDebug(@"WebRTC reported local SDP '%@', sending to '%@': %@", [RTCSessionDescription stringForType:sdp.type], self.fullRemoteJid, sdp.sdp); - //see https://webrtc.googlesource.com/src/+/refs/heads/main/sdk/objc/api/peerconnection/RTCSessionDescription.h + NSArray* children = [HelperTools sdp2xml:sdp.sdp withInitiator:YES]; + //we don't encrypt anything if encryption is not enabled for this contact or if the remote did not send us their deviceid + if(self.contact.isEncrypted && self.remoteOmemoDeviceId != nil && [self encryptFingerprintsInChildren:children]) + { + //we are encrypted now (if the remote can't decrypt this or answers with a cleartext fingerprint, we throw a security error later on) + self.encryptionState = [self encryptionTypeForDeviceid:self.remoteOmemoDeviceId]; + } + else + self.encryptionState = MLCallEncryptionStateClear; XMPPIQ* sdpIQ = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid]; [sdpIQ addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{ @"action": @"session-initiate", @"sid": self.jmiid, - } andChildren:[HelperTools sdp2xml:sdp.sdp withInitiator:YES] - andData:nil]]; + } andChildren:children andData:nil]]; /* TODO: implement raw sdp alongside jingle and write xep [[MLXMLNode alloc] initWithElement:@"sdp" andNamespace:@"urn:tmp:monal:webrtc:sdp:0" withAttributes:@{ @"id": self.jmiid, @@ -833,9 +878,18 @@ -(void) sendJmiProceed DDLogDebug(@"Accepting via JMI: %@", self); //xep 0353 mandates bare jid, but daniel will update it to mandate full jid XMPPMessage* jmiNode = [[XMPPMessage alloc] initWithType:kMessageChatType to:self.fullRemoteJid]; - [jmiNode addChildNode:[[MLXMLNode alloc] initWithElement:@"proceed" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{ + MLXMLNode* proceedElement = [[MLXMLNode alloc] initWithElement:@"proceed" andNamespace:@"urn:xmpp:jingle-message:0" withAttributes:@{ @"id": self.jmiid, - } andChildren:@[] andData:nil]]; + } andChildren:@[] andData:nil]; + //only offer omemo deviceid for encryption if encryption is enabled for this contact + if(self.contact.isEncrypted) + { + //see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd + [proceedElement addChildNode:[[MLXMLNode alloc] initWithElement:@"device" andNamespace:@"http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification" withAttributes:@{ + @"id": [self.account.omemo getDeviceId], + } andChildren:@[] andData:nil]]; + } + [jmiNode addChildNode:proceedElement]; [jmiNode setStoreHint]; self.jmiProceed = jmiNode; [self.account send:jmiNode]; @@ -912,22 +966,29 @@ -(NSString*) description case MLCallStateUnknown: state = @"unknown"; break; default: state = @"undefined"; break; } - return [NSString stringWithFormat:@"%@Call:%@", self.direction == MLCallDirectionIncoming ? @"Incoming" : @"Outgoing", @{ - @"uuid": self.uuid, - @"jmiid": self.jmiid, - @"state": state, - @"finishReason": @(self.finishReason), - @"durationTime": @(self.durationTime), - @"contact": nilWrapper(self.contact), - @"fullRemoteJid": nilWrapper(self.fullRemoteJid), - @"jmiPropose": nilWrapper(self.jmiPropose), - @"jmiProceed": nilWrapper(self.jmiProceed), - @"webRTCClient": nilWrapper(self.webRTCClient), - @"providerAnswerAction": nilWrapper(self.providerAnswerAction), - @"wasConnectedOnce": bool2str(self.wasConnectedOnce), - @"isConnected": bool2str(self.isConnected), - @"isReconnecting": bool2str(self.isReconnecting), - }]; + return [NSString stringWithFormat:@"%@Call:%@", + self.direction == MLCallDirectionIncoming ? @"Incoming" : @"Outgoing", + @{ + @"uuid": self.uuid, + @"jmiid": self.jmiid, + @"state": state, + @"finishReason": @(self.finishReason), + @"durationTime": @(self.durationTime), + @"contact": nilWrapper(self.contact), + @"fullRemoteJid": nilWrapper(self.fullRemoteJid), + @"jmiPropose": nilWrapper(self.jmiPropose), + @"jmiProceed": nilWrapper(self.jmiProceed), + @"webRTCClient": nilWrapper(self.webRTCClient), + @"providerAnswerAction": nilWrapper(self.providerAnswerAction), + @"wasConnectedOnce": bool2str(self.wasConnectedOnce), + @"isConnected": bool2str(self.isConnected), + @"isReconnecting": bool2str(self.isReconnecting), + @"hasLocalSDP": bool2str(self.localSDP != nil), + @"hasRemoteSDP": bool2str(self.remoteSDP != nil), + @"remoteOmemoDeviceId": nilWrapper(self.remoteOmemoDeviceId), + @"encryptionState": @(self.encryptionState), + } + ]; } -(NSString*) short @@ -1002,7 +1063,7 @@ -(void) webRTCClient:(WebRTCClient*) webRTCClient didDiscoverLocalCandidate:(RTC -(void) webRTCClient:(WebRTCClient*) webRTCClient didChangeConnectionState:(RTCIceConnectionState) state { @synchronized(self) { - if(webRTCClient != self.webRTCClient) + if(webRTCClient != self.webRTCClient && !self.isFinished) { DDLogInfo(@"Ignoring new RTCIceConnectionState %ld for webRTCClient: %@ (call migrated)", (long)state, webRTCClient); return; @@ -1124,9 +1185,8 @@ -(void) processIncomingICECandidate:(NSNotification*) notification -(void) processRemoteICECandidate:(XMPPIQ*) iqNode { - //TODO: all code in this method only allows for one single candidate per jingle transport-info iq, but the xep allows multiple candidates! - RTCIceCandidate* incomingCandidate = nil; + /* if([iqNode check:@"{urn:xmpp:jingle:1}jingle/{urn:tmp:monal:webrtc:candidate:0}candidate"]) { NSString* rawSDP = [[NSString alloc] initWithData:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/{urn:tmp:monal:webrtc:candidate:0}candidate#|base64"] encoding:NSUTF8StringEncoding]; @@ -1134,7 +1194,7 @@ -(void) processRemoteICECandidate:(XMPPIQ*) iqNode NSString* sdpMid = [[NSString alloc] initWithData:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/{urn:tmp:monal:webrtc:candidate:0}candidate@sdpMid|base64"] encoding:NSUTF8StringEncoding]; incomingCandidate = [[RTCIceCandidate alloc] initWithSdp:rawSDP sdpMLineIndex:[sdpMLineIndex intValue] sdpMid:sdpMid]; } - else + else*/ { NSString* rawSdp = [HelperTools xml2candidate:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:self.direction==MLCallDirectionIncoming]; if(rawSdp == nil) @@ -1166,16 +1226,19 @@ -(void) processRemoteICECandidate:(XMPPIQ*) iqNode } if(incomingCandidate == nil) { - DDLogError(@"incomingCandidate is unexpectedly nil!"); - XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; - [errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[ - [[MLXMLNode alloc] initWithElement:@"bad-request" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"], - ] andData:nil]]; - [self.account send:errorIq]; - - //don't be too harsh and not end the call here - //[self handleEndCallActionWithReason:MLCallFinishReasonError]; + DDLogError(@"incomingCandidate is unexpectedly nil, ignoring!"); + [self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]]; return; + +// XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; +// [errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[ +// [[MLXMLNode alloc] initWithElement:@"bad-request" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"], +// ] andData:nil]]; +// [self.account send:errorIq]; +// +// //don't be too harsh and not end the call here +// //[self handleEndCallActionWithReason:MLCallFinishReasonError]; +// return; } DDLogInfo(@"%@: Got remote ICE candidate for call: %@", self, incomingCandidate); NSString* remoteUfrag = [self.remoteSDP findFirst:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport@ufrag", incomingCandidate.sdpMid]; @@ -1241,24 +1304,44 @@ -(void) processIncomingSDP:(NSNotification*) notification return; } + //make sure we don't handle incoming sdp twice + if(self.remoteSDP != nil && [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"]) + { + DDLogWarn(@"Got new remote sdp but we already got one, ignoring! MITM/DDOS??"); + XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; + [errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"cancel"} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"conflict" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"], + ] andData:nil]]; + [self.account send:errorIq]; + return; + } + NSString* rawSDP; NSString* type; - //raw sdp alongside jingle mode + /*//raw sdp alongside jingle mode if([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/{urn:tmp:monal:webrtc:sdp:0}sdp"]) { rawSDP = [[NSString alloc] initWithData:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/{urn:tmp:monal:webrtc:sdp:0}sdp#|base64"] encoding:NSUTF8StringEncoding]; type = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/{urn:tmp:monal:webrtc:sdp:0}sdp@type"]; } - else + else*/ { - //handle candidates in initial sdp (our webrtc lib does not like them --> fake transport-info iqs for these) - //(candidates in initial jingle are allowed by xep!) - if([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] || [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"]) + if([iqNode check:@"{urn:xmpp:jingle:1}jingle"]) { - // don't change iqNode directly to not influence code outside of this method - MLXMLNode* copyWithoutCandidates = [iqNode copy]; + if( + ([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] && self.direction != MLCallDirectionOutgoing) || + ([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] && self.direction != MLCallDirectionIncoming) + ) { + DDLogWarn(@"Unexpected incoming jingle data direction, ignoring: %@", iqNode); + return; + } + + //don't change iqNode directly to not influence code outside of this method + iqNode = [iqNode copy]; + //handle candidates in initial sdp (our webrtc lib does not like them --> fake transport-info iqs for these) + //(candidates in initial jingle are allowed by xep!) @synchronized(self.candidateQueue) { - for(MLXMLNode* content in [copyWithoutCandidates find:@"{urn:xmpp:jingle:1}jingle/content"]) + for(MLXMLNode* content in [iqNode find:@"{urn:xmpp:jingle:1}jingle/content"]) { MLXMLNode* transport = [content findFirst:@"{urn:xmpp:jingle:transports:ice-udp:1}transport"]; for(MLXMLNode* candidate in [transport find:@"{urn:xmpp:jingle:transports:ice-udp:1}candidate"]) @@ -1279,26 +1362,71 @@ -(void) processIncomingSDP:(NSNotification*) notification } } } - // don't change iqNode directly to not influence code outside of this method - iqNode = (XMPPIQ*)copyWithoutCandidates; + //decrypt fingerprint, if needed (use iqNode copy created above to not influence code outside of this method) + //only decrypt if encryption is enabled for this contact + if(self.contact.isEncrypted) + { + //if this is a session-initiate and we can decrypt the fingerprint using the given deviceid, this call is encrypted now + //if we can NOT decrypt anything, but have a remote deviceid (e.g. the iq contains an omemo envelope), this is a security error + if([iqNode check:@"{urn:xmpp:jingle:1}jingle"]) + { + //save omemo deviceid if we got a session-initiate for this (incoming) call + self.remoteOmemoDeviceId = [iqNode findFirst:@"{urn:xmpp:jingle:1}jingle/{urn:xmpp:jingle:1}content/{urn:xmpp:jingle:transports:ice-udp:1}transport/{http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification}fingerprint/{eu.siacs.conversations.axolotl}encrypted/header@sid|uint"]; + if(self.remoteOmemoDeviceId != nil) + { + if([self decryptFingerprintsInIqNode:iqNode]) + self.encryptionState = [self encryptionTypeForDeviceid:self.remoteOmemoDeviceId]; + else + { + DDLogError(@"Could not decrypt remote SDP session-initiate fingerprint with OMEMO!"); + XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; + [errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"], + [[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" andData:@"Could not decrypt call with OMEMO!"], + ] andData:nil]]; + [self.account send:errorIq]; + + [self handleEndCallActionWithReason:MLCallFinishReasonSecurityError]; + return; + } + } + else + self.encryptionState = MLCallEncryptionStateClear; + } + + //if this is a session-accept after sending an encrypted session-initiate and we can NOT decrypt the fingerprint, + //this call is a security error (if we can decrypt it, everything is fine and the call is secured) + if([iqNode check:@"{urn:xmpp:jingle:1}jingle"]) + { + //we don't need to check self.remoteOmemoDeviceId, because self.encryptionState will only be different to + //MLCallEncryptionStateClear if the deviceid is not nil + if(self.encryptionState != MLCallEncryptionStateClear && ![self decryptFingerprintsInIqNode:iqNode]) + { + DDLogError(@"Could not decrypt remote SDP session-accept fingerprint with OMEMO!"); + XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; + [errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"], + [[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" andData:@"Could not decrypt call with OMEMO!"], + ] andData:nil]]; + [self.account send:errorIq]; + + [self handleEndCallActionWithReason:MLCallFinishReasonSecurityError]; + return; + } + } + } + else + self.encryptionState = MLCallEncryptionStateClear; } + + //now handle the jingle offer/response or terminate nodes and convert jingle xml to sdp if([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"]) { - if(self.direction != MLCallDirectionOutgoing) - { - DDLogWarn(@"Unexpected incoming jingle data direction, ignoring: %@", iqNode); - return; - } type = @"answer"; rawSDP = [HelperTools xml2sdp:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:NO]; } else if([iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"]) { - if(self.direction != MLCallDirectionIncoming) - { - DDLogWarn(@"Unexpected incoming jingle data direction, ignoring: %@", iqNode); - return; - } type = @"offer"; rawSDP = [HelperTools xml2sdp:[iqNode findFirst:@"{urn:xmpp:jingle:1}jingle"] withInitiator:YES]; } @@ -1332,6 +1460,8 @@ -(void) processIncomingSDP:(NSNotification*) notification [self handleEndCallActionWithReason:MLCallFinishReasonError]; return; } + + //convert raw sdp string to RTCSessionDescription object RTCSessionDescription* resultSDP = [[RTCSessionDescription alloc] initWithType:[RTCSessionDescription typeForString:type] sdp:rawSDP]; if(resultSDP == nil) { @@ -1346,6 +1476,9 @@ -(void) processIncomingSDP:(NSNotification*) notification return; } DDLogInfo(@"%@: Got remote SDP for call: %@", self, resultSDP); + @synchronized(self.candidateQueue) { + self.remoteSDP = iqNode; + } //this is blocking (e.g. no need for an inner @synchronized) weakify(self); @@ -1361,41 +1494,57 @@ -(void) processIncomingSDP:(NSNotification*) notification [self.account send:errorIq]; [self handleEndCallActionWithReason:MLCallFinishReasonError]; + return; } else { DDLogDebug(@"Successfully passed SDP to webRTCClient..."); - @synchronized(self.candidateQueue) { - self.remoteSDP = iqNode; - } - [self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]]; //only send a "session-accept" if the remote is the initiator (e.g. this is an incoming call) if(self.direction == MLCallDirectionIncoming) { - //it seems we have to create an offer and ignore it before we can create the desired answer - [self.webRTCClient offerWithCompletion:^(RTCSessionDescription* _) { - [self.webRTCClient answerWithCompletion:^(RTCSessionDescription* localSdp) { - DDLogDebug(@"Sending SDP answer back..."); - XMPPIQ* sdpIQ = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid]; - [sdpIQ addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{ - @"action": @"session-accept", - @"sid": self.jmiid, - } andChildren:[HelperTools sdp2xml:localSdp.sdp withInitiator:NO] - andData:nil]]; - [self.account send:sdpIQ]; + [self.webRTCClient answerWithCompletion:^(RTCSessionDescription* localSdp) { + DDLogDebug(@"Sending SDP answer back..."); + NSArray* children = [HelperTools sdp2xml:localSdp.sdp withInitiator:NO]; + //we got a session-initiate jingle iq + //--> self.encryptionState will NOT be MLCallEncryptionStateClear, if that iq contained an encrypted fingerprint, + //--> self.encryptionState WILL be MLCallEncryptionStateClear, if it did not contain such an encrypted fingerprint + //(in this case we just don't try to decrypt anything, the call will simply be unencrypted but continue) + //we don't need to check self.remoteOmemoDeviceId, because self.encryptionState will only be different to + //MLCallEncryptionStateClear if the deviceid is not nil + if(self.encryptionState != MLCallEncryptionStateClear && ![self encryptFingerprintsInChildren:children]) + { + DDLogError(@"Could not encrypt local SDP response fingerprint with OMEMO!"); + XMPPIQ* errorIq = [[XMPPIQ alloc] initAsErrorTo:iqNode]; + [errorIq addChildNode:[[MLXMLNode alloc] initWithElement:@"error" withAttributes:@{@"type": @"modify"} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"not-acceptable" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas"], + [[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-stanzas" andData:@"Could not encrypt call with OMEMO!"], + ] andData:nil]]; + [self.account send:errorIq]; - @synchronized(self.candidateQueue) { - self.localSDP = sdpIQ; - - DDLogDebug(@"Now handling queued candidate iqs: %lu", (unsigned long)self.candidateQueue.count); - for(XMPPIQ* candidateIq in self.candidateQueue) - [self processRemoteICECandidate:candidateIq]; - } - }]; + [self handleEndCallActionWithReason:MLCallFinishReasonSecurityError]; + return; + } + [self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]]; + + XMPPIQ* sdpIQ = [[XMPPIQ alloc] initWithType:kiqSetType to:self.fullRemoteJid]; + [sdpIQ addChildNode:[[MLXMLNode alloc] initWithElement:@"jingle" andNamespace:@"urn:xmpp:jingle:1" withAttributes:@{ + @"action": @"session-accept", + @"sid": self.jmiid, + } andChildren:children andData:nil]]; + [self.account send:sdpIQ]; + + @synchronized(self.candidateQueue) { + self.localSDP = sdpIQ; + + DDLogDebug(@"Now handling queued candidate iqs: %lu", (unsigned long)self.candidateQueue.count); + for(XMPPIQ* candidateIq in self.candidateQueue) + [self processRemoteICECandidate:candidateIq]; + } }]; } else { + [self.account send:[[XMPPIQ alloc] initAsResponseTo:iqNode]]; @synchronized(self.candidateQueue) { DDLogDebug(@"Now handling queued candidate iqs: %lu", (unsigned long)self.candidateQueue.count); for(XMPPIQ* candidateIq in self.candidateQueue) @@ -1422,4 +1571,78 @@ -(void) handleAudioRouteChangeNotification:(NSNotification*) notification self.speaker = NO; } +-(MLCallEncryptionState) encryptionTypeForDeviceid:(NSNumber* _Nonnull) deviceid +{ + NSNumber* trustLevel = [self.account.omemo getTrustLevelForJid:self.contact.contactJid andDeviceId:deviceid]; + if(trustLevel == nil) + return MLCallEncryptionStateClear; + switch(trustLevel.intValue) + { + case MLOmemoTrusted: return MLCallEncryptionStateTrusted; + case MLOmemoToFU: return MLCallEncryptionStateToFU; + default: return MLCallEncryptionStateClear; + } +} + +-(BOOL) encryptFingerprintsInChildren:(NSArray*) children +{ + //don't try to encrypt if the remote deviceid is not trusted + if([self encryptionTypeForDeviceid:self.remoteOmemoDeviceId] == MLCallEncryptionStateClear) + return NO; + + //see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd + BOOL retval = NO; + for(MLXMLNode* child in children) + for(MLXMLNode* fingerprint in [child find:@"/{urn:xmpp:jingle:1}content/{urn:xmpp:jingle:transports:ice-udp:1}transport/{urn:xmpp:jingle:apps:dtls:0}fingerprint"]) + { + MLXMLNode* envelope = [self.account.omemo encryptString:fingerprint.data toDeviceids:@{ + self.contact.contactJid: [NSSet setWithArray:@[self.remoteOmemoDeviceId]], + }]; + if(envelope == nil) + { + DDLogWarn(@"Could not encrypt fingerprint with OMEMO!"); + return NO; + } + [fingerprint addChildNode:envelope]; + [fingerprint setXMLNS:@"http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"]; + fingerprint.data = nil; + retval = YES; + } + //this is only true if at least one fingerprint could be found and encrypted (this is normally true) + return retval; +} + +-(BOOL) decryptFingerprintsInIqNode:(XMPPIQ*) iqNode +{ + //don't try to decrypt if the remote deviceid is not trusted + if([self encryptionTypeForDeviceid:self.remoteOmemoDeviceId] == MLCallEncryptionStateClear) + return NO; + + //see https://gist.github.com/iNPUTmice/aa4fc0aeea6ce5fb0e0fe04baca842cd + BOOL retval = NO; + for(MLXMLNode* fingerprintNode in [iqNode find:@"{urn:xmpp:jingle:1}jingle/content/{urn:xmpp:jingle:transports:ice-udp:1}transport/{http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification}fingerprint"]) + { + //more than one omemo envelope means we are under attack + if([[fingerprintNode find:@"{eu.siacs.conversations.axolotl}encrypted"] count] > 1) + { + DDLogWarn(@"More than one OMEMO envelope found!"); + return NO; + } + NSString* decryptedFingerprint = [self.account.omemo decryptOmemoEnvelope:[fingerprintNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted"] forSenderJid:self.contact.contactJid andReturnErrorString:NO]; + if(decryptedFingerprint == nil) + { + DDLogWarn(@"Could not decrypt OMEMO encrypted fingerprint!"); + return NO; + } + //remove omemo envelope, correct xmlns and add our decrypted fingerprint back in as text content + [fingerprintNode removeChildNode:[fingerprintNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted"]]; + [fingerprintNode setXMLNS:@"urn:xmpp:jingle:apps:dtls:0"]; + fingerprintNode.data = decryptedFingerprint; + retval = YES; + } + //this is only true if at least one fingerprint could be found and decrypted + //(that could be false, if the remote did something weird or a MITM changed something) + return retval; +} + @end diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 8dc5851596..b7069b09dd 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -20,7 +20,6 @@ #define DDLogStdout(frmt, ...) LOG_MAYBE(NO, ddLogLevel, LOG_FLAG_STDOUT, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__) static const DDLogLevel ddLogLevel = LOG_LEVEL_STDOUT; #import "MLLogFileManager.h" -#import "MLLogFormatter.h" #import "MLFileLogger.h" diff --git a/Monal/Classes/MLFileLogger.m b/Monal/Classes/MLFileLogger.m index d881d9a577..f19613ccb8 100644 --- a/Monal/Classes/MLFileLogger.m +++ b/Monal/Classes/MLFileLogger.m @@ -115,7 +115,7 @@ -(NSData*) lt_dataForMessage:(DDLogMessage*) logMessage //encode log message NSError* error; - NSData* rawData = [HelperTools convertLogmessageToJsonData:logMessage usingFormatter:_logFormatter counter:&counter andError:&error]; + NSData* rawData = [HelperTools convertLogmessageToJsonData:logMessage counter:&counter andError:&error]; if(error != nil || rawData == nil) { NSLog(@"Error jsonifying log message: %@, logMessage: %@", error, logMessage); diff --git a/Monal/Classes/MLLogFormatter.h b/Monal/Classes/MLLogFormatter.h deleted file mode 100755 index 9964127109..0000000000 --- a/Monal/Classes/MLLogFormatter.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// MLLogFormatter.h -// monalxmpp -// -// Created by Thilo Molitor on 27.07.20. -// Copyright © 2020 Monal.im. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface MLLogFormatter : DDDispatchQueueLogFormatter - --(NSString*) formatLogMessage:(DDLogMessage*) logMessage; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/MLLogFormatter.m b/Monal/Classes/MLLogFormatter.m deleted file mode 100755 index 37e0ca7a5b..0000000000 --- a/Monal/Classes/MLLogFormatter.m +++ /dev/null @@ -1,70 +0,0 @@ -// -// MLLogFormatter.m -// monalxmpp -// -// Created by Thilo Molitor on 27.07.20. -// Copyright © 2020 Monal.im. All rights reserved. -// - -#import -#import -#import "MLConstants.h" -#import "MLLogFormatter.h" -#import "HelperTools.h" - -static DDQualityOfServiceName _qos_name(NSUInteger qos) { - switch ((qos_class_t) qos) { - case QOS_CLASS_USER_INTERACTIVE: return @"UI"; - case QOS_CLASS_USER_INITIATED: return @"IN"; - case QOS_CLASS_DEFAULT: return @"DF"; - case QOS_CLASS_UTILITY: return @"UT"; - case QOS_CLASS_BACKGROUND: return @"BG"; - default: return @"UN"; - } -} - -static inline NSString* _loglevel_name(NSUInteger flag) { - if(flag & DDLogLevelOff) - return @" OFF"; - else if(flag & DDLogLevelError) - return @" ERROR"; - else if(flag & DDLogLevelWarning) - return @" WARN"; - else if(flag & DDLogLevelInfo) - return @" INFO"; - else if(flag & DDLogLevelDebug) - return @" DEBUG"; - else if(flag & DDLogLevelVerbose) - return @" VERB"; - else if(flag & LOG_LEVEL_STDERR) - return @"STDERR"; - else if(flag & LOG_LEVEL_STDOUT) - return @"STDOUT"; - else if(flag & DDLogLevelAll) - return @" ALL"; - return @" UNKN"; -} - -@implementation MLLogFormatter - --(NSString*) formatLogMessage:(DDLogMessage*) logMessage -{ - NSArray* filePathComponents = [logMessage.file pathComponents]; - NSString* file = logMessage.file; - if([filePathComponents count]>1) - file = [NSString stringWithFormat:@"%@/%@", filePathComponents[[filePathComponents count]-2], filePathComponents[[filePathComponents count]-1]]; - NSString* timestamp = [self stringFromDate:logMessage.timestamp]; - NSString* queueThreadLabel = [HelperTools getQueueThreadLabelFor:logMessage]; - - //append the mach thread id if not already present - if(![queueThreadLabel isEqualToString:logMessage.threadID]) - queueThreadLabel = [NSString stringWithFormat:@"%@:%@", logMessage.threadID, queueThreadLabel]; - - //don't format stdout and stderr logmessages, e.g. don't add metadata to logline (only to json) - if(logMessage.flag & LOG_FLAG_STDOUT || logMessage.flag & LOG_FLAG_STDERR) - return logMessage.message; - - return [NSString stringWithFormat:@"%@ [%@] %@ [%@ (QOS:%@)] %@ at %@:%lu: %@", timestamp, _loglevel_name(logMessage.flag), [HelperTools isAppExtension] ? @"*appex*" : @"mainapp", queueThreadLabel, _qos_name(logMessage.qos), logMessage.function, file, (unsigned long)logMessage.line, logMessage.message]; -} - -@end diff --git a/Monal/Classes/MLOMEMO.h b/Monal/Classes/MLOMEMO.h index 538814e3b7..69e5a2f500 100644 --- a/Monal/Classes/MLOMEMO.h +++ b/Monal/Classes/MLOMEMO.h @@ -8,6 +8,7 @@ #import #import "OmemoState.h" +#import "MLSignalStore.h" NS_ASSUME_NONNULL_BEGIN @@ -16,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @class xmpp; @class XMPPMessage; @class XMPPIQ; +@class MLXMLNode; @interface MLOMEMO : NSObject @property (nonatomic, strong) OmemoState* state; @@ -28,7 +30,9 @@ NS_ASSUME_NONNULL_BEGIN /* * encrypting / decrypting messages */ +-(MLXMLNode* _Nullable) encryptString:(NSString* _Nullable) message toDeviceids:(NSDictionary*>*) contactDeviceMap; -(void) encryptMessage:(XMPPMessage*) messageNode withMessage:(NSString* _Nullable) message toContact:(NSString*) toContact; +-(NSString* _Nullable) decryptOmemoEnvelope:(MLXMLNode*) envelope forSenderJid:(NSString*) senderJid andReturnErrorString:(BOOL) returnErrorString; -(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticipantJid:(NSString* _Nullable) mucParticipantJid; -(NSSet*) knownDevicesForAddressName:(NSString*) addressName; @@ -36,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN -(void) addIdentityManually:(SignalAddress*) address identityKey:(NSData* _Nonnull) identityKey; -(void) updateTrust:(BOOL) trust forAddress:(SignalAddress*)address; -(NSNumber*) getTrustLevel:(SignalAddress*)address identityKey:(NSData*)identityKey; +-(NSNumber* _Nullable) getTrustLevelForJid:(NSString*) jid andDeviceId:(NSNumber*) deviceid; -(NSData*) getIdentityForAddress:(SignalAddress*) address; -(BOOL) isSessionBrokenForJid:(NSString*) jid andDeviceId:(NSNumber*) rid; -(void) deleteDeviceForSource:(NSString*) source andRid:(NSNumber*) rid; diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index 45c0b27769..5b4bbad08c 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -843,6 +843,59 @@ -(BOOL) generateNewKeysIfNeeded return NO; } +-(MLXMLNode* _Nullable) encryptString:(NSString* _Nullable) message toDeviceids:(NSDictionary*>*) contactDeviceMap +{ + + MLXMLNode* encrypted = [[MLXMLNode alloc] initWithElement:@"encrypted" andNamespace:@"eu.siacs.conversations.axolotl"]; + + MLEncryptedPayload* encryptedPayload; + if(message) + { + // Encrypt message + encryptedPayload = [AESGcm encrypt:[message dataUsingEncoding:NSUTF8StringEncoding] keySize:KEY_SIZE]; + if(encryptedPayload == nil) + { + showErrorOnAlpha(self.account, @"Could not encrypt normal message: AESGcm error"); + return nil; + } + [encrypted addChildNode:[[MLXMLNode alloc] initWithElement:@"payload" andData:[HelperTools encodeBase64WithData:encryptedPayload.body]]]; + } + else + { + //there is no message that can be encrypted -> create new session keys (e.g. this is a key transport message) + NSData* newKey = [AESGcm genKey:KEY_SIZE]; + NSData* newIv = [AESGcm genIV]; + if(newKey == nil || newIv == nil) + { + showErrorOnAlpha(self.account, @"Could not create key or iv"); + return nil; + } + encryptedPayload = [[MLEncryptedPayload alloc] initWithKey:newKey iv:newIv]; + if(encryptedPayload == nil) + { + showErrorOnAlpha(self.account, @"Could not encrypt transport message: AESGcm error"); + return nil; + } + } + + //add crypto header with our own deviceid + MLXMLNode* header = [[MLXMLNode alloc] initWithElement:@"header" withAttributes:@{ + @"sid": [NSString stringWithFormat:@"%u", self.monalSignalStore.deviceid], + } andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"iv" andData:[HelperTools encodeBase64WithData:encryptedPayload.iv]], + ] andData:nil]; + + //add encryption for all given contacts' devices + for(NSString* recipient in contactDeviceMap) + { + DDLogVerbose(@"Adding encryption for devices of %@: %@", recipient, contactDeviceMap[recipient]); + [self addEncryptionKeyForAllDevices:contactDeviceMap[recipient] encryptForJid:recipient withEncryptedPayload:encryptedPayload withXMLHeader:header]; + } + + [encrypted addChildNode:header]; + return encrypted; +} + -(void) encryptMessage:(XMPPMessage*) messageNode withMessage:(NSString* _Nullable) message toContact:(NSString*) toContact { MLAssert(self.signalContext != nil, @"signalContext should be initiated."); @@ -884,64 +937,36 @@ -(void) encryptMessage:(XMPPMessage*) messageNode withMessage:(NSString* _Nullab if(recipientDevices && recipientDevices.count > 0) contactDeviceMap[recipient] = recipientDevices; } - NSSet* myDevices = [NSSet setWithArray:[self.monalSignalStore knownDevicesForAddressName:self.account.connectionProperties.identity.jid]]; //check if we found omemo keys of at least one of the recipients or more than 1 own device, otherwise don't encrypt anything + NSSet* myDevices = [NSSet setWithArray:[self.monalSignalStore knownDevicesForAddressName:self.account.connectionProperties.identity.jid]]; if(contactDeviceMap.count > 0 || myDevices.count > 1) { - MLXMLNode* encrypted = [[MLXMLNode alloc] initWithElement:@"encrypted" andNamespace:@"eu.siacs.conversations.axolotl"]; - - MLEncryptedPayload* encryptedPayload; - if(message) - { - // Encrypt message - encryptedPayload = [AESGcm encrypt:[message dataUsingEncoding:NSUTF8StringEncoding] keySize:KEY_SIZE]; - if(encryptedPayload == nil) - { - showErrorOnAlpha(self.account, @"Could not encrypt message: AESGcm error"); - return; - } - [encrypted addChildNode:[[MLXMLNode alloc] initWithElement:@"payload" andData:[HelperTools encodeBase64WithData:encryptedPayload.body]]]; - } - else - { - //there is no message that can be encrypted -> create new session keys (e.g. this is a key transport message) - NSData* newKey = [AESGcm genKey:KEY_SIZE]; - NSData* newIv = [AESGcm genIV]; - if(newKey == nil || newIv == nil) - { - showErrorOnAlpha(self.account, @"Could not create key or iv"); - return; - } - encryptedPayload = [[MLEncryptedPayload alloc] initWithKey:newKey iv:newIv]; - if(encryptedPayload == nil) - { - showErrorOnAlpha(self.account, @"Could not encrypt message: AESGcm error"); - return; - } - } - - //add crypto header with our own deviceid - MLXMLNode* header = [[MLXMLNode alloc] initWithElement:@"header" withAttributes:@{ - @"sid": [NSString stringWithFormat:@"%u", self.monalSignalStore.deviceid], - } andChildren:@[ - [[MLXMLNode alloc] initWithElement:@"iv" andData:[HelperTools encodeBase64WithData:encryptedPayload.iv]], - ] andData:nil]; - - //add encryption for all of our recipients' devices - for(NSString* recipient in contactDeviceMap) + //add encryption for all of our own devices to contactDeviceMap + DDLogVerbose(@"Adding encryption for OWN (%@) devices to contactDeviceMap: %@", self.account.connectionProperties.identity.jid, myDevices); + contactDeviceMap[self.account.connectionProperties.identity.jid] = myDevices; + + //now encrypt everything to all collected deviceids + MLXMLNode* envelope = [self encryptString:message toDeviceids:contactDeviceMap]; + if(envelope == nil) { - DDLogVerbose(@"Adding encryption for devices of %@: %@", recipient, contactDeviceMap[recipient]); - [self addEncryptionKeyForAllDevices:contactDeviceMap[recipient] encryptForJid:recipient withEncryptedPayload:encryptedPayload withXMLHeader:header]; + DDLogError(@"Got nil envelope!"); + return; } - - //add encryption for all of our own devices - DDLogVerbose(@"Adding encryption for OWN (%@) devices: %@", self.account.connectionProperties.identity.jid, myDevices); - [self addEncryptionKeyForAllDevices:myDevices encryptForJid:self.account.connectionProperties.identity.jid withEncryptedPayload:encryptedPayload withXMLHeader:header]; + [messageNode addChildNode:envelope]; + } +} - [encrypted addChildNode:header]; - [messageNode addChildNode:encrypted]; +-(NSNumber* _Nullable) getTrustLevelForJid:(NSString*) jid andDeviceId:(NSNumber*) deviceid +{ + SignalAddress* address = [[SignalAddress alloc] initWithName:jid deviceId:(uint32_t)deviceid.unsignedIntValue]; + NSData* identity = [self.monalSignalStore getIdentityForAddress:address]; + if(!identity) + { + showErrorOnAlpha(self.account, @"Could not get Identity for: %@ device id %@", jid, deviceid); + return nil; } + return [self getTrustLevel:address identityKey:identity]; } -(void) addEncryptionKeyForAllDevices:(NSSet*) devices encryptForJid:(NSString*) encryptForJid withEncryptedPayload:(MLEncryptedPayload*) encryptedPayload withXMLHeader:(MLXMLNode*) xmlHeader @@ -996,48 +1021,33 @@ -(void) addEncryptionKeyForAllDevices:(NSSet*) devices encryptForJid: [self removeQueuedKeyTransportElementsFor:encryptForJid andDevices:usedRids]; } --(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticipantJid:(NSString* _Nullable) mucParticipantJid +-(NSString* _Nullable) decryptOmemoEnvelope:(MLXMLNode*) envelope forSenderJid:(NSString*) senderJid andReturnErrorString:(BOOL) returnErrorString { - if(![messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header"]) + DDLogVerbose(@"OMEMO envelope: %@", envelope); + + if(![envelope check:@"header"]) { - showErrorOnAlpha(self.account, @"DecryptMessage called but the message has no encryption header"); + showErrorOnAlpha(self.account, @"decryptOmemoEnvelope called but the envelope has no encryption header"); return nil; } - BOOL isKeyTransportElement = ![messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/payload"]; - - NSNumber* sid = [messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted/header@sid|uint"]; - NSString* senderJid = nil; - if([messageNode check:@"/"]) - { - if(mucParticipantJid == nil) - { - DDLogError(@"Could not get muc participant jid and corresponding signal address of muc participant '%@': %@", messageNode.from, mucParticipantJid); -#ifdef IS_ALPHA - return [NSString stringWithFormat:@"Could not get muc participant jid and corresponding signal address of muc participant '%@': %@", messageNode.from, mucParticipantJid]; -#else - return nil; -#endif - } - else - senderJid = mucParticipantJid; - } - else - senderJid = messageNode.fromUser; + + BOOL isKeyTransportElement = ![envelope check:@"payload"]; + NSNumber* sid = [envelope findFirst:@"header@sid|uint"]; SignalAddress* address = [[SignalAddress alloc] initWithName:senderJid deviceId:(uint32_t)sid.unsignedIntValue]; if(!self.signalContext) { showErrorOnAlpha(self.account, @"Missing signal context in decrypt!"); - return NSLocalizedString(@"Error decrypting message", @""); + return !returnErrorString ? nil : NSLocalizedString(@"Error decrypting message", @""); } //don't try to decrypt our own messages (could be mirrored by MUC etc.) if([senderJid isEqualToString:self.account.connectionProperties.identity.jid] && sid.unsignedIntValue == self.monalSignalStore.deviceid) return nil; - NSData* messageKey = [messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted/header/key#|base64", self.monalSignalStore.deviceid]; - BOOL devicePreKey = [[messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted/header/key@prekey|bool", self.monalSignalStore.deviceid] boolValue]; + NSData* messageKey = [envelope findFirst:@"header/key#|base64", self.monalSignalStore.deviceid]; + BOOL devicePreKey = [[envelope findFirst:@"header/key@prekey|bool", self.monalSignalStore.deviceid] boolValue]; DDLogVerbose(@"Decrypting using:\nrid=%u --> messageKey=%@\nrid=%u --> isPreKey=%@", self.monalSignalStore.deviceid, messageKey, self.monalSignalStore.deviceid, bool2str(devicePreKey)); @@ -1050,7 +1060,7 @@ -(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticip { DDLogError(@"Message was not encrypted for this device: %u", self.monalSignalStore.deviceid); [self rebuildSessionWithJid:senderJid forRid:sid]; - return [NSString stringWithFormat:NSLocalizedString(@"Message was not encrypted for this device. Please make sure the sender trusts deviceid %u.", @""), self.monalSignalStore.deviceid]; + return !returnErrorString ? nil : [NSString stringWithFormat:NSLocalizedString(@"Message was not encrypted for this device. Please make sure the sender trusts deviceid %u.", @""), self.monalSignalStore.deviceid]; } else { @@ -1080,10 +1090,10 @@ -(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticip [self rebuildSessionWithJid:senderJid forRid:sid]; #ifdef IS_ALPHA if(isKeyTransportElement) - return [NSString stringWithFormat:@"There was an error decrypting this encrypted KEY TRANSPORT message (Signal error). To resolve this, try sending an encrypted message to this person. (%@)", error]; + return !returnErrorString ? nil : [NSString stringWithFormat:@"There was an error decrypting this encrypted KEY TRANSPORT message (Signal error). To resolve this, try sending an encrypted message to this person. (%@)", error]; #endif if(!isKeyTransportElement) - return [NSString stringWithFormat:NSLocalizedString(@"There was an error decrypting this encrypted message (Signal error). To resolve this, try sending an encrypted message to this person. (%@)", @""), error]; + return !returnErrorString ? nil : [NSString stringWithFormat:NSLocalizedString(@"There was an error decrypting this encrypted message (Signal error). To resolve this, try sending an encrypted message to this person. (%@)", @""), error]; return nil; } NSData* key; @@ -1095,10 +1105,10 @@ -(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticip [self rebuildSessionWithJid:senderJid forRid:sid]; #ifdef IS_ALPHA if(isKeyTransportElement) - return @"There was an error decrypting this encrypted KEY TRANSPORT message (Signal error). To resolve this, try sending an encrypted message to this person."; + return !returnErrorString ? nil : @"There was an error decrypting this encrypted KEY TRANSPORT message (Signal error). To resolve this, try sending an encrypted message to this person."; #endif if(!isKeyTransportElement) - return NSLocalizedString(@"There was an error decrypting this encrypted message (Signal error). To resolve this, try sending an encrypted message to this person.", @""); + return !returnErrorString ? nil : NSLocalizedString(@"There was an error decrypting this encrypted message (Signal error). To resolve this, try sending an encrypted message to this person.", @""); return nil; } else @@ -1124,7 +1134,7 @@ -(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticip { DDLogInfo(@"KeyTransportElement received from jid: %@ device: %@", senderJid, sid); #ifdef IS_ALPHA - return [NSString stringWithFormat:@"ALPHA_DEBUG_MESSAGE: KeyTransportElement received from jid: %@ device: %@", senderJid, sid]; + return !returnErrorString ? nil : [NSString stringWithFormat:@"ALPHA_DEBUG_MESSAGE: KeyTransportElement received from jid: %@ device: %@", senderJid, sid]; #else return nil; #endif @@ -1141,23 +1151,23 @@ -(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticip if(key != nil) { - NSData* iv = [messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted/header/iv#|base64"]; - NSData* decodedPayload = [messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted/payload#|base64"]; + NSData* iv = [envelope findFirst:@"header/iv#|base64"]; + NSData* decodedPayload = [envelope findFirst:@"payload#|base64"]; if(iv == nil || iv.length != 12) { showErrorOnAlpha(self.account, @"Could not decrypt message: iv length: %lu", (unsigned long)iv.length); - return NSLocalizedString(@"Error while decrypting: iv.length != 12", @""); + return !returnErrorString ? nil : NSLocalizedString(@"Error while decrypting: iv.length != 12", @""); } if(decodedPayload == nil) { - return NSLocalizedString(@"Error: Received OMEMO message is empty", @""); + return !returnErrorString ? nil : NSLocalizedString(@"Error: Received OMEMO message is empty", @""); } NSData* decData = [AESGcm decrypt:decodedPayload withKey:key andIv:iv withAuth:auth]; if(decData == nil) { showErrorOnAlpha(self.account, @"Could not decrypt message with key that was decrypted. (GCM error)"); - return NSLocalizedString(@"Encrypted message was sent in an older format Monal can't decrypt. Please ask them to update their client. (GCM error)", @""); + return !returnErrorString ? nil : NSLocalizedString(@"Encrypted message was sent in an older format Monal can't decrypt. Please ask them to update their client. (GCM error)", @""); } else DDLogInfo(@"Successfully decrypted message, passing back cleartext string..."); @@ -1166,12 +1176,35 @@ -(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticip else { showErrorOnAlpha(self.account, @"Could not get omemo decryption key"); - return NSLocalizedString(@"Could not decrypt message", @""); + return !returnErrorString ? nil : NSLocalizedString(@"Could not decrypt message", @""); } } } } +-(NSString* _Nullable) decryptMessage:(XMPPMessage*) messageNode withMucParticipantJid:(NSString* _Nullable) mucParticipantJid +{ + NSString* senderJid = nil; + if([messageNode check:@"/"]) + { + if(mucParticipantJid == nil) + { + DDLogError(@"Could not get muc participant jid and corresponding signal address of muc participant '%@': %@", messageNode.from, mucParticipantJid); +#ifdef IS_ALPHA + return [NSString stringWithFormat:@"Could not get muc participant jid and corresponding signal address of muc participant '%@': %@", messageNode.from, mucParticipantJid]; +#else + return nil; +#endif + } + else + senderJid = mucParticipantJid; + } + else + senderJid = messageNode.fromUser; + + return [self decryptOmemoEnvelope:[messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted"] forSenderJid:senderJid andReturnErrorString:YES]; +} + $$instance_handler(handleDevicelistUnsubscribe, account.omemo, $$ID(xmpp*, account), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(success == NO) { diff --git a/Monal/Classes/MLServerDetails.m b/Monal/Classes/MLServerDetails.m index a0724e96cf..1b4dea7f9d 100644 --- a/Monal/Classes/MLServerDetails.m +++ b/Monal/Classes/MLServerDetails.m @@ -81,6 +81,13 @@ -(void) checkServerCaps:(MLXMPPConnection*) connection @"Color": connection.supportsPing ? @"Green" : @"Red" }]; + // supportsExternalServiceDiscovery + [self.serverCaps addObject:@{ + @"Title":NSLocalizedString(@"XEP-0215: External Service Discovery", @""), + @"Description":NSLocalizedString(@"XMPP protocol extension for discovering services external to the XMPP network, like STUN or TURN servers needed for A/V calls.", @""), + @"Color": connection.supportsPing ? @"Green" : @"Red" + }]; + // supportsRosterVersion [self.serverCaps addObject:@{ @"Title":NSLocalizedString(@"XEP-0237: Roster Versioning", @""), diff --git a/Monal/Classes/MLUDPLogger.m b/Monal/Classes/MLUDPLogger.m index 3cc0c799ce..81d19776ab 100644 --- a/Monal/Classes/MLUDPLogger.m +++ b/Monal/Classes/MLUDPLogger.m @@ -233,16 +233,13 @@ -(void) logMessage:(DDLogMessage*) logMessage return; NSError* error = nil; - NSData* rawData = [HelperTools convertLogmessageToJsonData:logMessage usingFormatter:self->_logFormatter counter:&counter andError:&error]; + NSData* rawData = [HelperTools convertLogmessageToJsonData:logMessage counter:&counter andError:&error]; if(error != nil || rawData == nil) { [[self class] logError:@"json encode error: %@", error]; return; } - //you have to uncomment the following line to send only the formatted logline - //rawData = [logMsg dataUsingEncoding:NSUTF8StringEncoding]; - //compress data to account for udp size limits rawData = [self gzipDeflate:rawData]; diff --git a/Monal/Classes/MLXMLNode.m b/Monal/Classes/MLXMLNode.m index b4d487bd8d..39546f7fae 100644 --- a/Monal/Classes/MLXMLNode.m +++ b/Monal/Classes/MLXMLNode.m @@ -819,9 +819,9 @@ -(NSString*) XMLString for(NSString* key in [_attributes allKeys]) { //handle xmlns inheritance (don't add namespace to childs if it should be the same like the parent's one) - if([key isEqualToString:@"xmlns"] && parent && [_attributes[@"xmlns"] isEqualToString:parent.attributes[@"xmlns"]]) + if([key isEqualToString:@"xmlns"] && parent && [[NSString stringWithFormat:@"%@", _attributes[@"xmlns"]] isEqualToString:[NSString stringWithFormat:@"%@", parent.attributes[@"xmlns"]]]) continue; - [outputString appendString:[NSString stringWithFormat:@" %@='%@'", key, [MLXMLNode escapeForXMPP:(NSString*)_attributes[key]]]]; + [outputString appendString:[NSString stringWithFormat:@" %@='%@'", key, [MLXMLNode escapeForXMPP:[NSString stringWithFormat:@"%@", _attributes[key]]]]]; } if([_children count] || (_data && ![_data isEqualToString:@""])) diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index 20ef19bce3..acc6ab408e 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -81,10 +81,13 @@ public class ObservableKVOWrapper: ObservableObject { private func addObserverForMember(_ member: String){ if(!self.observedMembers.contains(member)) { DDLogDebug("Adding observer for member '\(member)'...") - self.observers.append(KVOObserver(obj:self.obj, keyPath:member, objectWillChange: { - DDLogDebug("Observer said '\(member)' has changed...") + self.observers.append(KVOObserver(obj:self.obj, keyPath:member, objectWillChange: { [weak self] in + guard let self = self else { + return + } + //DDLogDebug("Observer said '\(member)' has changed...") DispatchQueue.main.async { - DDLogDebug("Calling self.objectWillChange.send()...") + DDLogDebug("Calling self.objectWillChange.send() for '\(member)'...") self.objectWillChange.send() } })) @@ -94,7 +97,7 @@ public class ObservableKVOWrapper: ObservableObject { private func getWrapper(for member:String) -> AnyObject? { addObserverForMember(member) - DDLogDebug("Returning value for dynamicMember \(member): \(String(describing:self.obj.value(forKey:member)))") + //DDLogDebug("Returning value for dynamicMember \(member): \(String(describing:self.obj.value(forKey:member)))") return self.obj.value(forKey:member) as AnyObject? } diff --git a/Monal/Classes/WebRTCClient.swift b/Monal/Classes/WebRTCClient.swift index b7b4162709..2252ffe61d 100644 --- a/Monal/Classes/WebRTCClient.swift +++ b/Monal/Classes/WebRTCClient.swift @@ -45,6 +45,10 @@ final class WebRTCClient: NSObject { unreachable("WebRTCClient:init is unavailable") } + deinit { + DDLogDebug("Deinit of webrtc client for delegate: \(String(describing:self.delegate))") + } + @objc static func createPeerConnection(iceServers: [RTCIceServer], forceRelay: Bool) -> RTCPeerConnection? { let config = RTCConfiguration() @@ -86,6 +90,8 @@ final class WebRTCClient: NSObject { @objc required init(iceServers: [RTCIceServer], audioOnly: Bool, forceRelay: Bool) { + RTCSetMinDebugLogLevel(.info) + var peerConnection = WebRTCClient.createPeerConnection(iceServers: iceServers, forceRelay: forceRelay) if peerConnection == nil { // try again with empty ice server list diff --git a/Monal/Monal-Info.plist b/Monal/Monal-Info.plist index cdb24471ef..4bda9713d4 100644 --- a/Monal/Monal-Info.plist +++ b/Monal/Monal-Info.plist @@ -83,7 +83,7 @@ NSLocationWhenInUseUsageDescription Monal uses your location when you send a location message in a conversation. NSMicrophoneUsageDescription - Monal uses the microphone to transmit your voice in audio calls. + Monal uses the microphone to transmit your voice in audio messages or calls. NSPhotoLibraryAddUsageDescription Monal allows users to save photos received in conversations. NSPhotoLibraryUsageDescription diff --git a/Monal/Monal-iOS/Launch Screen.storyboard b/Monal/Monal-iOS/Launch Screen.storyboard index 2fdd2351fd..0398ef4ad6 100644 --- a/Monal/Monal-iOS/Launch Screen.storyboard +++ b/Monal/Monal-iOS/Launch Screen.storyboard @@ -1,9 +1,9 @@ - + - + @@ -19,7 +19,7 @@ -