null, no validation will be attempted
* @param requestClientIP
* used for throttling user creation requests coming from the same IP address
+ * @param enforceStrongPassword TODO
*/
User createSimpleUser(String username, String email, String password, String fullName, String company,
- Locale locale, String validationBaseURL, UserGroup userOwner, String requestClientIP)
+ Locale locale, String validationBaseURL, UserGroup userOwner, String requestClientIP, boolean enforceStrongPassword)
throws UserManagementException, MailException, UserGroupManagementException;
void updateSimpleUserPassword(String name, String newPassword) throws UserManagementException;
diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java
index fe67b4f15b1..0b14d835097 100755
--- a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java
+++ b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java
@@ -83,6 +83,13 @@ import org.scribe.builder.api.YahooApi;
import org.scribe.model.Token;
import org.scribe.oauth.OAuthService;
+import com.nulabinc.zxcvbn.StandardDictionaries;
+import com.nulabinc.zxcvbn.StandardKeyboards;
+import com.nulabinc.zxcvbn.Strength;
+import com.nulabinc.zxcvbn.Zxcvbn;
+import com.nulabinc.zxcvbn.ZxcvbnBuilder;
+import com.nulabinc.zxcvbn.io.ClasspathResource;
+import com.nulabinc.zxcvbn.matchers.SlantedKeyboardLoader;
import com.sap.sse.ServerInfo;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
@@ -273,13 +280,15 @@ implements ReplicableSecurityService, ClearStateTestSupport {
/**
* Contains locking objects keyed by client IP addresses ({@code null} not allowed) that describe which client IPs
* are locked for {@link User} creation, e.g., through the
- * {@link #createSimpleUser(String, String, String, String, String, Locale, String, UserGroup, String)
+ * {@link #createSimpleUser(String, String, String, String, String, Locale, String, UserGroup, String, boolean)
* createSimpleUser} method.
*
* When entering values into this map, the method entering it is responsible for also scheduling a background
* task that a while after lock expiry the record is expunged again from the map to avoid garbage piling up.
*/
private final ConcurrentMap clientIPBasedLockingAndBanningForUserCreation;
+
+ private final Zxcvbn passwordValidator;
/**
* When working with a user's subscriptions, such as first reading, then changing and updating a user's subscription
@@ -359,6 +368,20 @@ implements ReplicableSecurityService, ClearStateTestSupport {
SecurityUtils.setSecurityManager(securityManager);
this.securityManager = securityManager;
aclResolver = new SecurityServiceAclResolver(accessControlStore);
+ try {
+ passwordValidator = createPasswordValidator();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Zxcvbn createPasswordValidator() throws IOException {
+ final ZxcvbnBuilder builder = new ZxcvbnBuilder();
+ builder.dictionaries(StandardDictionaries.loadAllDictionaries());
+ builder.keyboards(StandardKeyboards.loadAllKeyboards());
+ return builder
+ .keyboard(new SlantedKeyboardLoader("qwertz", new ClasspathResource("qwertz-keyboard.txt")).load())
+ .build();
}
@Override
@@ -453,7 +476,7 @@ implements ReplicableSecurityService, ClearStateTestSupport {
final User adminUser = createSimpleUser(UserStore.ADMIN_USERNAME, "nobody@sapsailing.com",
ADMIN_DEFAULT_PASSWORD,
/* fullName */ null, /* company */ null, Locale.ENGLISH, /* validationBaseURL */ null,
- null, /* clientIP */ null);
+ null, /* clientIP */ null, /* enforce strong password */ false);
setOwnership(adminUser.getIdentifier(), adminUser, null);
Role adminRole = new Role(adminRoleDefinition, /* transitive */ true);
addRoleForUserAndSetUserAsOwner(adminUser, adminRole);
@@ -1068,7 +1091,7 @@ implements ReplicableSecurityService, ClearStateTestSupport {
@Override
public User createSimpleUser(final String username, final String email, String password, String fullName,
String company, Locale locale, final String validationBaseURL, UserGroup groupOwningUser,
- String requestClientIP) throws UserManagementException, MailException, UserGroupManagementException {
+ String requestClientIP, boolean enforceStrongPassword) throws UserManagementException, MailException, UserGroupManagementException {
if (requestClientIP != null) {
checkAndRecordUserCreationFromClientIP(requestClientIP);
}
@@ -1079,7 +1102,7 @@ implements ReplicableSecurityService, ClearStateTestSupport {
}
if (username == null || username.length() < 3) {
throw new UserManagementException(UserManagementException.USERNAME_DOES_NOT_MEET_REQUIREMENTS);
- } else if (password == null || password.length() < 5) {
+ } else if (enforceStrongPassword && !isPasswordGoodEnough(password)) {
throw new UserManagementException(UserManagementException.PASSWORD_DOES_NOT_MEET_REQUIREMENTS);
}
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
@@ -1098,6 +1121,17 @@ implements ReplicableSecurityService, ClearStateTestSupport {
updateSimpleUserEmail(username, email, validationBaseURL);
return result;
}
+
+ private boolean isPasswordGoodEnough(String password) {
+ final boolean result;
+ if (password == null) {
+ result = false;
+ } else {
+ final Strength strength = passwordValidator.measure(password);
+ result = strength.getGuessesLog10() > 8;
+ }
+ return result;
+ }
/**
* Checks if the {@code clientIP} is currently blocked for user creation, and if so, throws a
@@ -1183,7 +1217,7 @@ implements ReplicableSecurityService, ClearStateTestSupport {
}
private void updateSimpleUserPassword(final User user, String newPassword) throws UserManagementException {
- if (newPassword == null || newPassword.length() < 5) {
+ if (!isPasswordGoodEnough(newPassword)) {
throw new UserManagementException(UserManagementException.PASSWORD_DOES_NOT_MEET_REQUIREMENTS);
}
// for non-admins, check that the old password is correct
diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/jaxrs/api/SecurityResource.java b/java/com.sap.sse.security/src/com/sap/sse/security/jaxrs/api/SecurityResource.java
index 538c2c9ec06..11700e1a358 100644
--- a/java/com.sap.sse.security/src/com/sap/sse/security/jaxrs/api/SecurityResource.java
+++ b/java/com.sap.sse.security/src/com/sap/sse/security/jaxrs/api/SecurityResource.java
@@ -202,7 +202,7 @@ public class SecurityResource extends AbstractSecurityResource {
final String fullNameToUse = preferFirstIfNotNullOrElseSecond(formFullName, queryFullName);
final String companyToUse = preferFirstIfNotNullOrElseSecond(formCompany, queryCompany);
User newUser = getSecurityService().createSimpleUser(usernameToUse, emailToUse, passwordToUse, fullNameToUse, companyToUse,
- Locale.ENGLISH, validationBaseURL, getSecurityService().getDefaultTenantForCurrentUser(), clientIP);
+ Locale.ENGLISH, validationBaseURL, getSecurityService().getDefaultTenantForCurrentUser(), clientIP, /* enforce strong password */ true);
SecurityUtils.getSubject().login(new UsernamePasswordToken(usernameToUse, passwordToUse));
return newUser;
}
diff --git a/java/com.sap.sse.test/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sse.test/.settings/org.eclipse.core.resources.prefs
index 99f26c0203a..1eac703ce9b 100644
--- a/java/com.sap.sse.test/.settings/org.eclipse.core.resources.prefs
+++ b/java/com.sap.sse.test/.settings/org.eclipse.core.resources.prefs
@@ -1,2 +1,3 @@
eclipse.preferences.version=1
+encoding//src/com/sap/sse/test/zxcvbn/GermanKeyboardTest.java=UTF-8
encoding/=UTF-8
diff --git a/java/com.sap.sse.test/META-INF/MANIFEST.MF b/java/com.sap.sse.test/META-INF/MANIFEST.MF
index 596bec7caa1..e82e8ad45c4 100644
--- a/java/com.sap.sse.test/META-INF/MANIFEST.MF
+++ b/java/com.sap.sse.test/META-INF/MANIFEST.MF
@@ -10,6 +10,7 @@ Require-Bundle: org.junit;bundle-version="4.8.2",
org.mockito.mockito-core,
net.bytebuddy.byte-buddy;bundle-version="1.14.12",
net.bytebuddy.byte-buddy-agent;bundle-version="1.14.12",
- org.objenesis;bundle-version="3.3.0"
+ org.objenesis;bundle-version="3.3.0",
+ com.nulab-inc.zxcvbn;bundle-version="1.9.0"
Automatic-Module-Name: com.sap.sse.test
Import-Package: javax.xml.bind
diff --git a/java/com.sap.sse.test/src/com/sap/sse/test/zxcvbn/GermanKeyboardTest.java b/java/com.sap.sse.test/src/com/sap/sse/test/zxcvbn/GermanKeyboardTest.java
new file mode 100644
index 00000000000..0247f6a0e6a
--- /dev/null
+++ b/java/com.sap.sse.test/src/com/sap/sse/test/zxcvbn/GermanKeyboardTest.java
@@ -0,0 +1,52 @@
+package com.sap.sse.test.zxcvbn;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import org.junit.Test;
+
+import com.nulabinc.zxcvbn.StandardDictionaries;
+import com.nulabinc.zxcvbn.StandardKeyboards;
+import com.nulabinc.zxcvbn.Zxcvbn;
+import com.nulabinc.zxcvbn.ZxcvbnBuilder;
+import com.nulabinc.zxcvbn.matchers.Keyboard;
+import com.nulabinc.zxcvbn.matchers.SlantedKeyboardLoader;
+
+public class GermanKeyboardTest {
+ private static final Logger logger = Logger.getLogger(GermanKeyboardTest.class.getName());
+
+ private static final String QWERTZ = "^° 1! 2\" 3§ 4$ 5% 6& 7/ 8( 9) 0= ß? ´`\n" +
+ " qQ wW eE rR tT zZ uU iI oO pP üÜ +*\n" +
+ " aA sS dD fF gG hH jJ kK lL öÖ äÄ #'\n" +
+ " yY xX cC vV bB nN mM ,; .: -_" ;
+
+ @Test
+ public void testGermanKeyboard() throws IOException {
+ final String DE_PASSWD1 = "klöä#";
+ final String DE_PASSWD2 = "iopü+";
+ logger.info("first test password: "+DE_PASSWD1);
+ logger.info("second test password: "+DE_PASSWD2);
+ final Keyboard germanKeyboard = new SlantedKeyboardLoader(DE_PASSWD1, ()->new ByteArrayInputStream(QWERTZ.getBytes())).load();
+ final ZxcvbnBuilder builder = new ZxcvbnBuilder();
+ builder.dictionaries(StandardDictionaries.loadAllDictionaries());
+ builder.keyboards(StandardKeyboards.loadAllKeyboards());
+ builder.keyboard(germanKeyboard);
+ final Zxcvbn zxcvbnWithGermanKeyboard = builder.build();
+ final Zxcvbn zxcvbn = new Zxcvbn();
+ {
+ final double defaultGuessesForQuertz = zxcvbn.measure(DE_PASSWD1).getGuesses();
+ final double germanGuessesForQuertz = zxcvbnWithGermanKeyboard.measure(DE_PASSWD1).getGuesses();
+ assertTrue(""+defaultGuessesForQuertz+" is not greater than "+germanGuessesForQuertz+" but should have been",
+ defaultGuessesForQuertz > germanGuessesForQuertz);
+ }
+ {
+ final double defaultGuessesForQuertz = zxcvbn.measure(DE_PASSWD2).getGuesses();
+ final double germanGuessesForQuertz = zxcvbnWithGermanKeyboard.measure(DE_PASSWD2).getGuesses();
+ assertTrue(""+defaultGuessesForQuertz+" is not greater than "+germanGuessesForQuertz+" but should have been",
+ defaultGuessesForQuertz > germanGuessesForQuertz);
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/pom.xml b/java/pom.xml
index ad2aad85df2..ef7f21a091c 100755
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -47,7 +47,7 @@
Can we move the system property -Dmongo.dbName to "parameters.mongodb"? If so the IP of the host would be the same for all
profiles. -->
- -Dfile.encoding=cp1252 -Dmongo.dbName=winddbTest -DspatialWind=true -Xss100m -Dgwt.rpc.version=9 -Dgoogle.maps.authenticationparams=key=AIzaSyD1Se4tIkt-wglccbco3S7twaHiG20hR9E
+ -Dfile.encoding=UTF-8 -Dmongo.dbName=winddbTest -DspatialWind=true -Xss100m -Dgwt.rpc.version=9 -Dgoogle.maps.authenticationparams=key=AIzaSyD1Se4tIkt-wglccbco3S7twaHiG20hR9E
-ea -Xmx8192m -XX:+UseG1GC