diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index 3f17c4ee8..ac1a229bf 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -793,6 +793,16 @@ not block logins, and it is enforced even when failed login Default value is `5`. Set to `0` to disable. +|S |SOGoMaximumFailedTotpCount +|Parameter used to control the number of wrong TOTP verification codes +allowed when logging in to the web interface. When reached, TOTP +verification is blocked for the account (even with a valid code) for +_SOGoFailedLoginBlockInterval_ seconds since the first wrong code. +This counter is independent of _SOGoMaximumFailedLoginCount_, which +only counts wrong passwords. + +Default value is `5`. Set to `0` to disable. + |S |SOGoMaximumMessageSubmissionCount |Parameter used to control the number of email messages a user can send from SOGo's webmail interface, to _SOGoMaximumRecipientCount_, in diff --git a/SoObjects/SOGo/SOGoCache.h b/SoObjects/SOGo/SOGoCache.h index c547cb414..b4ed84feb 100644 --- a/SoObjects/SOGo/SOGoCache.h +++ b/SoObjects/SOGo/SOGoCache.h @@ -97,6 +97,11 @@ - (NSDictionary *) failedPasswordRecoveryCountForLogin: (NSString *) login; +- (void) setFailedTotpCount: (int) theCount + forLogin: (NSString *) theLogin; + +- (NSDictionary *) failedTotpCountForLogin: (NSString *) login; + - (void) setMessageSubmissionsCount: (int) theCount recipientsCount: (int) theRecipientsCount forLogin: (NSString *) theLogin; diff --git a/SoObjects/SOGo/SOGoCache.m b/SoObjects/SOGo/SOGoCache.m index 12e3f1b43..c51fdc2ec 100644 --- a/SoObjects/SOGo/SOGoCache.m +++ b/SoObjects/SOGo/SOGoCache.m @@ -568,6 +568,23 @@ static memcached_st *handle = NULL; ofType: @"failedrecovery"]; } +// +// SOGo TOTP verification failed counts (web login second factor). +// +- (void) setFailedTotpCount: (int) theCount + forLogin: (NSString *) theLogin +{ + [self _setFailedCount: theCount + forLogin: theLogin + ofType: @"failedtotp"]; +} + +- (NSDictionary *) failedTotpCountForLogin: (NSString *) theLogin +{ + return [self _failedCountForLogin: theLogin + ofType: @"failedtotp"]; +} + // // // diff --git a/SoObjects/SOGo/SOGoDefaults.plist b/SoObjects/SOGo/SOGoDefaults.plist index 00f5a567b..b175113ca 100644 --- a/SoObjects/SOGo/SOGoDefaults.plist +++ b/SoObjects/SOGo/SOGoDefaults.plist @@ -209,4 +209,5 @@ SOGoPasswordRecoveryMode = "Disabled"; SOGoPasswordRecoveryQuestion = "SecretQuestion1"; SOGoMaximumFailedPasswordRecoveryCount = 5; + SOGoMaximumFailedTotpCount = 5; } diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index 65cf55bc7..ba15f4ed5 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -128,6 +128,7 @@ NSComparisonResult languageSort(id el1, id el2, void *context); - (int) maximumFailedLoginInterval; - (int) failedLoginBlockInterval; - (int) maximumFailedPasswordRecoveryCount; +- (int) maximumFailedTotpCount; - (int) maximumMessageSizeLimit; diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index 000ce66b4..89e0ea786 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -927,6 +927,17 @@ NSComparisonResult languageSort(id el1, id el2, void *context) return [self integerForKey: @"SOGoMaximumFailedPasswordRecoveryCount"]; } +// +// Number of wrong TOTP verification codes allowed during +// SOGoFailedLoginBlockInterval seconds before TOTP verification +// is blocked for the account. Defaults to 5 (see SOGoDefaults.plist), +// 0 disables the protection. +// +- (int) maximumFailedTotpCount +{ + return [self integerForKey: @"SOGoMaximumFailedTotpCount"]; +} + // // // diff --git a/SoObjects/SOGo/SOGoUserManager.h b/SoObjects/SOGo/SOGoUserManager.h index 6db033180..937de94fd 100644 --- a/SoObjects/SOGo/SOGoUserManager.h +++ b/SoObjects/SOGo/SOGoUserManager.h @@ -112,6 +112,8 @@ perr: (SOGoPasswordPolicyError *) perr; - (NSString *) getPasswordRecoveryTokenFor: (NSString *) username domain:(NSString *) domain; - (BOOL) isPasswordRecoveryTokenValidFor: (NSString *) token uid: (NSString *) uid; +- (BOOL) isTotpVerificationBlockedForLogin: (NSString *) login + failedCount: (int *) failedCount; @end #endif /* SOGOUSERMANAGER_H */ diff --git a/SoObjects/SOGo/SOGoUserManager.m b/SoObjects/SOGo/SOGoUserManager.m index 97913045e..a89256b25 100644 --- a/SoObjects/SOGo/SOGoUserManager.m +++ b/SoObjects/SOGo/SOGoUserManager.m @@ -1545,6 +1545,45 @@ static const NSString *kObfuscatedSecondaryEmailKey = @"obfuscatedSecondaryEmail return jwtToken; } +/** + * (TOTP) Check whether TOTP verification is temporarily blocked for login + * because of too many wrong codes. Resets the counter once the block interval is over. + * @param login User login (uid) + * @param failedCount Current number of failures (out) + * @return True if blocked + */ +- (BOOL) isTotpVerificationBlockedForLogin: (NSString *) login + failedCount: (int *) failedCount +{ + NSDictionary *failedTotp; + SOGoSystemDefaults *sd; + unsigned int currentTime, startTime; + int maxFailed; + + *failedCount = 0; + sd = [SOGoSystemDefaults sharedSystemDefaults]; + maxFailed = [sd maximumFailedTotpCount]; + if (maxFailed <= 0 || ![login length]) + return NO; + + failedTotp = [[SOGoCache sharedCache] failedTotpCountForLogin: login]; + if (!failedTotp) + return NO; + + currentTime = [[NSCalendarDate date] timeIntervalSince1970]; + startTime = [[failedTotp objectForKey: @"InitialDate"] unsignedIntValue]; + if (currentTime - startTime > (unsigned int) [sd failedLoginBlockInterval]) + { + [[SOGoCache sharedCache] setFailedTotpCount: 0 + forLogin: login]; + return NO; + } + + *failedCount = [[failedTotp objectForKey: @"FailedCount"] intValue]; + + return (*failedCount >= maxFailed); +} + /** * (Secret Question) Check whether password recovery is temporarily blocked for uid * because of too many wrong answers. Resets the counter once the block interval is over. diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index 3fcb667d5..928e5d4e3 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -325,11 +325,22 @@ static const NSString *kJwtKey = @"jwt"; if ([ud totpEnabled]) { NSString *verificationCode; + int totpFailedCount; verificationCode = [params objectForKey: @"verificationCode"]; if ([verificationCode length] == 6 && [verificationCode unsignedIntValue] > 0) { unsigned int code; + + // Too many wrong codes: refuse any code until the block interval is over + if ([[SOGoUserManager sharedUserManager] isTotpVerificationBlockedForLogin: [loggedInUser login] + failedCount: &totpFailedCount]) + { + [self logWithFormat: @"TOTP verification from '%@' for user '%@' blocked - too many failed attempts", + remoteHost, username]; + return [self _responseWithLDAPPolicyError: PolicyAccountLocked additionalInfos: nil]; + } + const char *real_secret; char *secret; @@ -407,6 +418,9 @@ static const NSString *kJwtKey = @"jwt"; if (old_code != [verificationCode unsignedIntValue]) { [self logWithFormat: @"Invalid TOTP key for '%@'", username]; + if ([[SOGoSystemDefaults sharedSystemDefaults] maximumFailedTotpCount] > 0) + [[SOGoCache sharedCache] setFailedTotpCount: totpFailedCount + 1 + forLogin: [loggedInUser login]]; [json setObject: [NSNumber numberWithInt: 1] forKey: @"totpInvalidKey"]; return [self responseWithStatus: 403 andJSONRepresentation: json]; } @@ -415,6 +429,11 @@ static const NSString *kJwtKey = @"jwt"; [us setTotpKey: [[us userPrivateSalt] substringToIndex: 12]]; } } + + // Valid code: reset the failed TOTP count + if (totpFailedCount > 0) + [[SOGoCache sharedCache] setFailedTotpCount: 0 + forLogin: [loggedInUser login]]; } else {