diff --git a/SoObjects/Appointments/SOGoCalendarComponent.m b/SoObjects/Appointments/SOGoCalendarComponent.m index 44d15c3f5..4fa3a1bdb 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.m +++ b/SoObjects/Appointments/SOGoCalendarComponent.m @@ -234,7 +234,7 @@ tags = [NSArray arrayWithObjects: @"DTSTAMP", @"DTSTART", @"DTEND", @"DUE", @"EXDATE", @"EXRULE", @"RRULE", @"RECURRENCE-ID", nil]; uid = [[component uid] asCryptedPassUsingScheme: @"ssha256" - withSalt: [[settings userSalt] dataUsingEncoding: NSASCIIStringEncoding] + withSalt: [[settings userPublicSalt] dataUsingEncoding: NSASCIIStringEncoding] andEncoding: encHex keyPath: nil]; diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index dc9b7773a..fe7751fcd 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -1138,7 +1138,7 @@ size_t s_len, secret_len; - key = [[[self userSettings] userSalt] substringToIndex: 12]; + key = [[[self userSettings] userPrivateSalt] substringToIndex: 12]; s = [key UTF8String]; s_len = strlen(s); diff --git a/SoObjects/SOGo/SOGoUserSettings.h b/SoObjects/SOGo/SOGoUserSettings.h index 7a3958ae3..973022660 100644 --- a/SoObjects/SOGo/SOGoUserSettings.h +++ b/SoObjects/SOGo/SOGoUserSettings.h @@ -1,6 +1,6 @@ /* SOGoUserSettings.h - this file is part of SOGo * - * Copyright (C) 2009-2016 Inverse inc. + * Copyright (C) 2009-2021 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,7 +33,8 @@ - (NSArray *) subscribedCalendars; - (NSArray *) subscribedAddressBooks; -- (NSString *) userSalt; +- (NSString *) userPrivateSalt; +- (NSString *) userPublicSalt; @end diff --git a/SoObjects/SOGo/SOGoUserSettings.m b/SoObjects/SOGo/SOGoUserSettings.m index eb71d84c1..f03b77488 100644 --- a/SoObjects/SOGo/SOGoUserSettings.m +++ b/SoObjects/SOGo/SOGoUserSettings.m @@ -1,6 +1,6 @@ /* SOGoUserSettings.m - this file is part of SOGo * - * Copyright (C) 2009-2016 Inverse inc. + * Copyright (C) 2009-2021 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -70,7 +70,7 @@ static Class SOGoUserProfileKlass = Nil; return [self _subscribedFoldersForModule: @"Contacts"]; } -- (NSString *) userSalt +- (NSString *) userPublicSalt { NSMutableDictionary *values; NSString *salt; @@ -93,5 +93,27 @@ static Class SOGoUserProfileKlass = Nil; return salt; } +- (NSString *) userPrivateSalt +{ + NSMutableDictionary *values; + NSString *salt; + + salt = [[self dictionaryForKey: @"General"] objectForKey: @"PrivateSalt"]; + + if (!salt) + { + salt = [[[NSProcessInfo processInfo] globallyUniqueString] asSHA1String]; + values = [self objectForKey: @"General"]; + + if (!values) + values = [NSMutableDictionary dictionary]; + + [values setObject: salt forKey: @"PrivateSalt"]; + [self setObject: values forKey: @"General"]; + [self synchronize]; + } + + return salt; +} @end diff --git a/UI/MainUI/English.lproj/Localizable.strings b/UI/MainUI/English.lproj/Localizable.strings index c13c28b56..b8afe01b4 100644 --- a/UI/MainUI/English.lproj/Localizable.strings +++ b/UI/MainUI/English.lproj/Localizable.strings @@ -26,6 +26,7 @@ "Verification Code" = "Verification Code"; "Enter the 6-digit verification code from your TOTP application." = "Enter the 6-digit verification code from your TOTP application."; "You provided an invalid TOTP key." = "You provided an invalid TOTP key."; +"Two-factor authentication has been disabled. Visit the Preferences module to restore two-factor authentication and reconfigure your TOTP application." = "Two-factor authentication has been disabled. Visit the Preferences module to restore two-factor authentication and reconfigure your TOTP application."; "Download" = "Download"; "Language" = "Language"; diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index fdf30249c..38a03d34b 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -202,6 +202,7 @@ WOCookie *authCookie, *xsrfCookie; SOGoWebAuthenticator *auth; SOGoUserDefaults *ud; + SOGoUserSettings *us; SOGoUser *loggedInUser; NSDictionary *params; NSString *username, *password, *language, *domain, *remoteHost; @@ -235,7 +236,7 @@ || (expire < 0 && grace > 0) // password expired, grace still permits login || (expire >= 0 && grace == -1))) // password about to expire OR ppolicy activated and passwd never changed { - NSDictionary *json; + NSMutableDictionary *json = [NSMutableDictionary dictionary]; [self logWithFormat: @"successful login from '%@' for user '%@' - expire = %d grace = %d", remoteHost, username, expire, grace]; @@ -248,9 +249,10 @@ username = [[SOGoUserManager sharedUserManager] getUIDForEmail: username]; loggedInUser = [SOGoUser userWithLogin: username]; + ud = [loggedInUser userDefaults]; #if defined(MFA_CONFIG) - if ([[loggedInUser userDefaults] totpEnabled]) + if ([ud totpEnabled]) { NSString *verificationCode; @@ -295,29 +297,45 @@ if (code != [verificationCode unsignedIntValue]) { [self logWithFormat: @"Invalid TOTP key for '%@'", username]; - json = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: 1] - forKey: @"totpInvalidKey"]; + [json setObject: [NSNumber numberWithInt: 1] + forKey: @"totpInvalidKey"]; return [self responseWithStatus: 403 - andJSONRepresentation: json]; + andJSONRepresentation: json]; } } // if ([verificationCode length] == 6 && [verificationCode unsignedIntValue] > 0) else { - [self logWithFormat: @"Missing TOTP key for '%@', asking it..", username]; - json = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: 1] - forKey: @"totpMissingKey"]; - return [self responseWithStatus: 202 - andJSONRepresentation: json]; + us = [loggedInUser userSettings]; + if ([us dictionaryForKey: @"General"] && ![[us dictionaryForKey: @"General"] objectForKey: @"PrivateSalt"]) + { + // Since v5.3.0, a new salt is used for TOTP. If it's missing, disable TOTP and alert the user. + [ud setTotpEnabled: NO]; + [ud synchronize]; + + [self logWithFormat: @"New TOTP key for '%@' must be created", username]; + [json setObject: [NSNumber numberWithInt: 1] + forKey: @"totpDisabled"]; + } + else + { + [self logWithFormat: @"Missing TOTP key for '%@', asking it..", username]; + [json setObject: [NSNumber numberWithInt: 1] + forKey: @"totpMissingKey"]; + return [self responseWithStatus: 202 + andJSONRepresentation: json]; + } } } #endif [self _checkAutoReloadWebCalendars: loggedInUser]; - json = [NSDictionary dictionaryWithObjectsAndKeys: - [loggedInUser cn], @"cn", - [NSNumber numberWithInt: expire], @"expire", - [NSNumber numberWithInt: grace], @"grace", nil]; + [json setObject: [loggedInUser cn] + forKey: @"cn"]; + [json setObject: [NSNumber numberWithInt: expire] + forKey: @"expire"]; + [json setObject: [NSNumber numberWithInt: grace] + forKey: @"grace"]; response = [self responseWithStatus: 200 andJSONRepresentation: json]; @@ -339,7 +357,6 @@ [context setActiveUser: loggedInUser]; if (language && [supportedLanguages containsObject: language]) { - ud = [loggedInUser userDefaults]; [ud setLanguage: language]; [ud synchronize]; } diff --git a/UI/Templates/MainUI/SOGoRootPage.wox b/UI/Templates/MainUI/SOGoRootPage.wox index 7991f2868..6c14e7216 100644 --- a/UI/Templates/MainUI/SOGoRootPage.wox +++ b/UI/Templates/MainUI/SOGoRootPage.wox @@ -126,8 +126,8 @@ - +
@@ -154,6 +154,27 @@
+ +
+
+ warning +
+ {{app.cn}} +
+
+ priority_high +
+ +
+
+
+ +
+
+
@@ -193,20 +214,25 @@ -
- warning -
- {{app.cn}} +
+ warning +
+ {{app.cn}} +
+
+ priority_high +
{{app.errorMessage}}
+
+
+ + +
-
- {{app.errorMessage}} - -
-
diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index 2be2dd970..54b308bd8 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -252,7 +252,7 @@
+ ng-show="app.preferences.defaults.SOGoTOTPEnabled == 1">
diff --git a/UI/WebServerResources/js/Common/Authentication.service.js b/UI/WebServerResources/js/Common/Authentication.service.js index 0ace92957..0c605fc8b 100644 --- a/UI/WebServerResources/js/Common/Authentication.service.js +++ b/UI/WebServerResources/js/Common/Authentication.service.js @@ -97,6 +97,13 @@ if (typeof data.totpMissingKey != 'undefined' && response.status == 202) { d.resolve({totpmissingkey: 1}); } + else if (typeof data.totpDisabled != 'undefined') { + d.resolve({ + cn: data.cn, + url: redirectUrl(username, domain), + totpdisabled: 1 + }); + } // Check password policy else if (typeof data.expire != 'undefined' && typeof data.grace != 'undefined') { if (data.expire < 0 && data.grace > 0) { diff --git a/UI/WebServerResources/js/Common/sgQrCode.directive.js b/UI/WebServerResources/js/Common/sgQrCode.directive.js index 61c745139..709c94de7 100644 --- a/UI/WebServerResources/js/Common/sgQrCode.directive.js +++ b/UI/WebServerResources/js/Common/sgQrCode.directive.js @@ -26,6 +26,7 @@ function link(scope, element, attrs) { var width = parseInt(scope.width) || 256, height = parseInt(scope.height) || width, + // See https://github.com/google/google-authenticator/wiki/Key-Uri-Format uri = 'otpauth://totp/SOGo:' + Settings.activeUser('email') + '?secret=' + scope.text.replace(/=+$/, '') + '&issuer=SOGo'; new QRCode(element[0], { text: uri, diff --git a/UI/WebServerResources/js/Main/Main.app.js b/UI/WebServerResources/js/Main/Main.app.js index 92d850a65..8580e1635 100644 --- a/UI/WebServerResources/js/Main/Main.app.js +++ b/UI/WebServerResources/js/Main/Main.app.js @@ -44,6 +44,11 @@ if (data.totpmissingkey) { vm.loginState = 'totpcode'; } + else if (data.totpdisabled) { + vm.loginState = 'totpdisabled'; + vm.cn = data.cn; + vm.url = data.url; + } else { vm.loginState = 'logged'; vm.cn = data.cn;