diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 6b4f2d9d8..1b6cc9270 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -247,9 +247,11 @@ -(BOOL) updateAccounWithDictionary:(NSDictionary*) dictionary NSString* query = @"UPDATE account SET server=?, other_port=?, username=?, resource=?, domain=?, enabled=?, directTLS=?, rosterName=?, statusMessage=?, needs_password_migration=? WHERE account_id=?;"; NSString* server = (NSString*)[dictionary objectForKey:kServer]; NSString* port = (NSString*)[dictionary objectForKey:kPort]; + if ([port isEqual:@""]) + port = nil; NSArray* params = @[ - server == nil ? @"" : server, - port == nil ? @"5222" : port, + nilDefault(server, @""), + nilDefault(port, @"5222"), ((NSString*)[dictionary objectForKey:kUsername]), ((NSString*)[dictionary objectForKey:kResource]), ((NSString*)[dictionary objectForKey:kDomain]), @@ -272,9 +274,11 @@ -(NSNumber*) addAccountWithDictionary:(NSDictionary*) dictionary NSString* query = @"INSERT INTO account (server, other_port, resource, domain, enabled, directTLS, username, rosterName, statusMessage, plain_activated) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; NSString* server = (NSString*) [dictionary objectForKey:kServer]; NSString* port = (NSString*)[dictionary objectForKey:kPort]; + if ([port isEqual:@""]) + port = nil; NSArray* params = @[ - server == nil ? @"" : server, - port == nil ? @"5222" : port, + nilDefault(server, @""), + nilDefault(port, @"5222"), ((NSString *)[dictionary objectForKey:kResource]), ((NSString *)[dictionary objectForKey:kDomain]), [dictionary objectForKey:kEnabled] , diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index 48f010f3b..326b12032 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -366,15 +366,16 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) else { switch(indexPath.row - [self getAccountNum]) { - case QuickSettingsRow: - { + case QuickSettingsRow: { UIViewController* loginView = [[SwiftuiInterface new] makeViewWithName:@"LogIn"]; [self showDetailViewController:loginView sender:self]; break; } - case AdvancedSettingsRow: - [self performSegueWithIdentifier:@"editXMPP" sender:self]; + case AdvancedSettingsRow: { + UIViewController* advancedLoginView = [[SwiftuiInterface new] makeViewWithName:@"AdvancedLogIn"]; + [self showDetailViewController:advancedLoginView sender:self]; break; + } default: unreachable(); } diff --git a/Monal/Classes/MLXMPPManager.h b/Monal/Classes/MLXMPPManager.h index 511f2583b..ab5df898f 100644 --- a/Monal/Classes/MLXMPPManager.h +++ b/Monal/Classes/MLXMPPManager.h @@ -87,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN -(NSDate *) connectedTimeFor:(NSNumber*) accountID; -(NSNumber* _Nullable) login:(NSString*) jid password:(NSString*) password; +-(NSNumber* _Nullable) login:(NSString*) jid password:(NSString*) password hardcodedServer:(NSString* _Nullable) hardcodedServer hardcodedPort:(NSString* _Nullable) hardcodedPort forceDirectTLS:(BOOL) directTLS allowPlainAuth:(BOOL) plainActivated; -(void) removeAccountForAccountID:(NSNumber*) accountID; -(void) addNewAccountToKeychainAndConnectWithPassword:(NSString*) password andAccountID:(NSNumber*) accountID; diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index ad97a1617..2083a2f07 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -715,6 +715,26 @@ -(void) sendChatState:(BOOL) isTyping toContact:(MLContact*) contact #pragma mark - login/register -(NSNumber*) login:(NSString*) jid password:(NSString*) password +{ + NSArray* elements = [jid componentsSeparatedByString:@"@"]; + MLAssert([elements count] > 1, @"Got invalid jid", (@{@"jid": nilWrapper(jid), @"elements": elements})); + NSString* domain = ((NSString*)[elements objectAtIndex:1]).lowercaseString; + + //we don't want to set kPlainActivated (not even according to our preload list) and default to plain_activated=false, + //because the error message will warn the user and direct them to the advanced account creation menu to activate PLAIN + //if they still want to connect to this server + //only exception: yax.im --> we don't want to suggest a server during account creation that has a scary warning + //when logging in using another device afterwards + //TODO: to be removed once yax.im and quicksy.im supports SASL2 and SSDP!! + //TODO: use preload list and allow PLAIN for all others once enough domains are on this list + //allow plain for all servers not on preload list, since prosody with SASL2 wasn't even released yet + BOOL defaultPlainActivated = YES; + BOOL plainActivated = ([domain isEqualToString:@"yax.im"] || [domain isEqualToString:@"quicksy.im"]) ? YES : defaultPlainActivated; + + return [self login:jid password:password hardcodedServer:nil hardcodedPort:nil forceDirectTLS:NO allowPlainAuth:plainActivated]; +} + +-(NSNumber*) login:(NSString*) jid password:(NSString*) password hardcodedServer:(NSString*) hardcodedServer hardcodedPort:(NSString*) hardcodedPort forceDirectTLS:(BOOL) directTLS allowPlainAuth:(BOOL) plainActivated { //check if it is a JID NSArray* elements = [jid componentsSeparatedByString:@"@"]; @@ -739,17 +759,12 @@ -(NSNumber*) login:(NSString*) jid password:(NSString*) password [dic setObject:user forKey:kUsername]; [dic setObject:[HelperTools encodeRandomResource] forKey:kResource]; [dic setObject:@YES forKey:kEnabled]; - [dic setObject:@NO forKey:kDirectTLS]; - //we don't want to set kPlainActivated (not even according to our preload list) and default to plain_activated=false, - //because the error message will warn the user and direct them to the advanced account creation menu to activate PLAIN - //if they still want to connect to this server - //only exception: yax.im --> we don't want to suggest a server during account creation that has a scary warning - //when logging in using another device afterwards - //TODO: to be removed once yax.im and quicksy.im supports SASL2 and SSDP!! - //TODO: use preload list and allow PLAIN for all others once enough domains are on this list - //allow plain for all servers not on preload list, since prosody with SASL2 wasn't even released yet - NSNumber* defaultPlainActivated = @YES; - [dic setObject:([domain isEqualToString:@"yax.im"] || [domain isEqualToString:@"quicksy.im"] ? @YES : defaultPlainActivated) forKey:kPlainActivated]; + if(hardcodedServer != nil) + [dic setObject:hardcodedServer forKey:kServer]; + if(hardcodedPort != nil) + [dic setObject:hardcodedPort forKey:kPort]; + [dic setObject:@(directTLS) forKey:kDirectTLS]; + [dic setObject:@(plainActivated) forKey:kPlainActivated]; NSNumber* accountID = [[DataLayer sharedInstance] addAccountWithDictionary:dic]; if(accountID == nil) diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index d02aa51f1..34d94e208 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -816,6 +816,8 @@ class SwiftuiInterface : NSObject { host = UIHostingController(rootView:AnyView(AddTopLevelNavigation(withDelegate:delegate, to:WelcomeLogIn(delegate:delegate)))) case "LogIn": host = UIHostingController(rootView:AnyView(UIKitWorkaround(WelcomeLogIn(delegate:delegate)))) + case "AdvancedLogIn": + host = UIHostingController(rootView:AnyView(UIKitWorkaround(WelcomeLogIn(advancedMode: true, delegate: delegate)))) case "ChatPlaceholder": host = UIHostingController(rootView:AnyView(ChatPlaceholder())) case "GeneralSettings" : diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index f8f641629..36af32ead 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -9,6 +9,7 @@ struct WelcomeLogIn: View { static private let credFaultyPattern = "^.+@.+\\..{2,}$" + var advancedMode: Bool = false var delegate: SheetDismisserProtocol @State private var isEditingJid: Bool = false @@ -16,6 +17,11 @@ struct WelcomeLogIn: View { @State private var isEditingPassword: Bool = false @State private var password: String = "" + @State private var hardcodedServer: String = "" + @State private var hardcodedPort: String = "5222" + @State private var allowPlainAuth: Bool = false + @State private var forceDirectTLS: Bool = false + @State private var showAlert = false @State private var showQRCodeScanner = false @@ -26,7 +32,7 @@ struct WelcomeLogIn: View { @State private var loginComplete = false @State private var isLoadingOmemoBundles = false - @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) + @State private var alertPrompt = AlertPrompt() @StateObject private var overlay = LoadingOverlayState() #if IS_ALPHA @@ -40,18 +46,21 @@ struct WelcomeLogIn: View { private var credentialsEnteredAlert: Bool { alertPrompt.title = Text("Empty Values!") alertPrompt.message = Text("Please make sure you have entered both a username and password.") + alertPrompt.dismissLabel = Text("Close") return credentialsEntered } private var credentialsFaultyAlert: Bool { alertPrompt.title = Text("Invalid Credentials!") alertPrompt.message = Text("Your XMPP jid should be in in the format user@domain.tld. For special configurations, use manual setup.") + alertPrompt.dismissLabel = Text("Close") return credentialsFaulty } private var credentialsExistAlert: Bool { alertPrompt.title = Text("Duplicate jid!") alertPrompt.message = Text("This account already exists in Monal.") + alertPrompt.dismissLabel = Text("Close") return credentialsExist } @@ -60,6 +69,7 @@ struct WelcomeLogIn: View { hideLoadingOverlay(overlay) alertPrompt.title = Text("Timeout Error") alertPrompt.message = Text("We were not able to connect your account. Please check your username and password and make sure you are connected to the internet.") + alertPrompt.dismissLabel = Text("Close") showAlert = true } @@ -67,6 +77,7 @@ struct WelcomeLogIn: View { hideLoadingOverlay(overlay) alertPrompt.title = Text("Success!") alertPrompt.message = Text("You are set up and connected.") + alertPrompt.dismissLabel = Text("Close") showAlert = true } @@ -74,9 +85,22 @@ struct WelcomeLogIn: View { hideLoadingOverlay(overlay) alertPrompt.title = Text("Error") alertPrompt.message = Text(String(format: NSLocalizedString("We were not able to connect your account. Please check your username and password and make sure you are connected to the internet.\n\nTechnical error message: %@", comment: ""), errorMessage)) + alertPrompt.dismissLabel = Text("Close") showAlert = true } + private func showPlainAuthWarningAlert() { + alertPrompt.title = Text("Warning") + alertPrompt.message = Text("If you turn this on, you will no longer be safe from man-in-the-middle attacks. Such attacks enable the adversary to manipulate your incoming and outgoing messages, add their own OMEMO keys, change your account details and even know or change your password!\n\nYou should rather switch to another server than turning this on.") + alertPrompt.dismissLabel = Text("Understood") + showAlert = true + } + + private var jidDomainPart: String { + let jidComponents = HelperTools.splitJid(jid) + return jidComponents["host"] ?? "" + } + private var credentialsEntered: Bool { return !jid.isEmpty && !password.isEmpty } @@ -119,22 +143,23 @@ struct WelcomeLogIn: View { GeometryReader { proxy in ScrollView { VStack(alignment: .leading) { - VStack { - HStack () { - Image(decorative: appLogoId) - .resizable() - .frame(width: CGFloat(120), height: CGFloat(120), alignment: .center) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) - .padding() - - Text("Log in to your existing account or register a new account. If required you will find more advanced options in Monal settings.") - .padding() - .padding(.leading, -16.0) - + if !advancedMode { + VStack { + HStack () { + Image(decorative: appLogoId) + .resizable() + .frame(width: CGFloat(120), height: CGFloat(120), alignment: .center) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .padding() + + Text("Log in to your existing account or register a new account. If required you will find more advanced options in Monal settings.") + .padding() + .padding(.leading, -16.0) + } } + .frame(maxWidth: .infinity) + .background(Color(UIColor.systemBackground)) } - .frame(maxWidth: .infinity) - .background(Color(UIColor.systemBackground)) Form { Text("I already have an account:") @@ -154,7 +179,53 @@ struct WelcomeLogIn: View { SecureField(NSLocalizedString("Password", comment: "placeholder when adding account"), text: $password) .addClearButton(isEditing: password.count > 0, text: $password) .listRowSeparator(.hidden) - + + if advancedMode { + TextField("Optional Hardcoded Hostname", text: $hardcodedServer) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.URL) + .addClearButton(isEditing: hardcodedServer.count > 0, text: $hardcodedServer) + .listRowSeparator(.hidden) + + if !hardcodedServer.isEmpty { + HStack { + Text("Port") + Spacer() + TextField("Optional Hardcoded Port", text: $hardcodedPort) + .keyboardType(.numberPad) + .addClearButton(isEditing: hardcodedPort.count > 0, text: $hardcodedPort) + .onDisappear { + hardcodedPort = "5222" + } + } + .listRowSeparator(.hidden) + + Toggle(isOn: $forceDirectTLS) { + Text("Always use direct TLS, not STARTTLS") + } + .onDisappear { + forceDirectTLS = false + } + } + + Toggle(isOn: $allowPlainAuth) { + Text("Allow MITM-prone PLAIN authentication") + } + // TODO: use the SCRAM preload list instead of hardcoding servers + .disabled(["conversations.im"].contains(jidDomainPart.lowercased())) + .onChange(of: jid) { _ in + if ["conversations.im"].contains(jidDomainPart.lowercased()) { + allowPlainAuth = false + } + } + .onChange(of: allowPlainAuth) { _ in + if allowPlainAuth { + showPlainAuthWarningAlert() + } + } + } + HStack() { Button(action: { showAlert = !credentialsEnteredAlert || credentialsFaultyAlert || credentialsExistAlert @@ -163,7 +234,11 @@ struct WelcomeLogIn: View { startLoginTimeout() showLoadingOverlay(overlay, headline:NSLocalizedString("Logging in", comment: "")) self.errorObserverEnabled = true - self.newAccountID = MLXMPPManager.sharedInstance().login(self.jid, password: self.password) + if advancedMode { + self.newAccountID = MLXMPPManager.sharedInstance().login(self.jid, password: self.password, hardcodedServer:self.hardcodedServer, hardcodedPort:self.hardcodedPort, forceDirectTLS: self.forceDirectTLS, allowPlainAuth: self.allowPlainAuth) + } else { + self.newAccountID = MLXMPPManager.sharedInstance().login(self.jid, password: self.password) + } if(self.newAccountID == nil) { currentTimeout = nil // <- disable timeout on error errorObserverEnabled = false @@ -188,36 +263,44 @@ struct WelcomeLogIn: View { })) } - // Just sets the credential in jid and password variables and shows them in the input fields - // so user can control what they scanned and if o.k. login via the "Login" button. - Button(action: { - showQRCodeScanner = true - }){ - Image(systemName: "qrcode") - .frame(maxHeight: .infinity) - .padding(9.0) - .background(Color(UIColor.tertiarySystemFill)) - .foregroundColor(.primary) - .clipShape(Circle()) - } - .buttonStyle(BorderlessButtonStyle()) - .sheet(isPresented: $showQRCodeScanner) { - Text("QR-Code Scanner").font(.largeTitle.weight(.bold)) - // Get existing credentials from QR and put values in jid and password - MLQRCodeScanner( - handleLogin: { jid, password in - self.jid = jid - self.password = password - }, handleClose: { - self.showQRCodeScanner = false - } - ) + if !advancedMode { + // Just sets the credential in jid and password variables and shows them in the input fields + // so user can control what they scanned and if o.k. login via the "Login" button. + Button(action: { + showQRCodeScanner = true + }){ + Image(systemName: "qrcode") + .frame(maxHeight: .infinity) + .padding(9.0) + .background(Color(UIColor.tertiarySystemFill)) + .foregroundColor(.primary) + .clipShape(Circle()) + } + .buttonStyle(BorderlessButtonStyle()) + .sheet(isPresented: $showQRCodeScanner) { + Text("QR-Code Scanner").font(.largeTitle.weight(.bold)) + // Get existing credentials from QR and put values in jid and password + MLQRCodeScanner( + handleLogin: { jid, password in + self.jid = jid + self.password = password + }, handleClose: { + self.showQRCodeScanner = false + } + ) + } } + } - + .listRowSeparator(.hidden, edges: .top) + // Align the (bottom) list row separator to the very left + .alignmentGuide(.listRowSeparatorLeading) { _ in + return 0 + } + NavigationLink(destination: LazyClosureView(RegisterAccount(delegate: self.delegate))) { Text("Register a new account") - .foregroundColor(Color.accentColor) + .foregroundColor(Color.accentColor) } if(DataLayer.sharedInstance().enabledAccountCnts() == 0) { @@ -243,7 +326,8 @@ struct WelcomeLogIn: View { } } .addLoadingOverlay(overlay) - .navigationBarTitle(Text("Welcome"), displayMode:.large) + .navigationTitle(advancedMode ? Text("Add Account (advanced)") : Text("Welcome")) + .navigationBarTitleDisplayMode(advancedMode ? .inline : .large) .onDisappear {UITableView.appearance().tableHeaderView = nil} //why that?? .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kXMPPError")).receive(on: RunLoop.main)) { notification in if(self.errorObserverEnabled == false) {