package org.opendaylight.controller.usermanager;
import java.io.Serializable;
-import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.opendaylight.controller.configuration.ConfigurationObject;
import org.opendaylight.controller.sal.authorization.AuthResultEnum;
+import org.opendaylight.controller.sal.packet.BitBufferHelper;
import org.opendaylight.controller.sal.utils.HexEncode;
import org.opendaylight.controller.sal.utils.Status;
import org.opendaylight.controller.sal.utils.StatusCode;
-import org.opendaylight.controller.usermanager.AuthResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Configuration Java Object which represents a Local AAA user configuration
* information for User Manager.
*/
-public class UserConfig implements Serializable {
+@XmlRootElement
+@XmlAccessorType(XmlAccessType.NONE)
+public class UserConfig extends ConfigurationObject implements Serializable {
private static final long serialVersionUID = 1L;
-
- protected String user;
- protected List<String> roles;
- private String password;
+ private static Logger log = LoggerFactory.getLogger(UserConfig.class);
+ private static final boolean strongPasswordCheck = Boolean.getBoolean("enableStrongPasswordCheck");
+ private static final String DIGEST_ALGORITHM = "SHA-384";
+ private static final String BAD_PASSWORD = "Bad Password";
private static final int USERNAME_MAXLENGTH = 32;
- private static final int PASSWORD_MINLENGTH = 5;
- private static final int PASSWORD_MAXLENGTH = 256;
+ protected static final String PASSWORD_REGEX = "(?=.*[^a-zA-Z0-9])(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,256}$";
private static final Pattern INVALID_USERNAME_CHARACTERS = Pattern.compile("([/\\s\\.\\?#%;\\\\]+)");
- private static MessageDigest oneWayFunction = null;
+ private static MessageDigest oneWayFunction;
+ private static SecureRandom randomGenerator;
+
static {
try {
- UserConfig.oneWayFunction = MessageDigest.getInstance("SHA-1");
+ UserConfig.oneWayFunction = MessageDigest.getInstance(DIGEST_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
+ log.error(String.format("Implementation of %s digest algorithm not found: %s", DIGEST_ALGORITHM,
+ e.getMessage()));
}
+ UserConfig.randomGenerator = new SecureRandom(BitBufferHelper.toByteArray(System.currentTimeMillis()));
}
+ /**
+ * User Id
+ */
+ @XmlElement
+ protected String user;
+
+ /**
+ * List of roles a user can have
+ * example
+ * System-Admin
+ * Network-Admin
+ * Network-Operator
+ */
+ @XmlElement
+ protected List<String> roles;
+
+ /**
+ * Password
+ * Should be 8 to 256 characters long,
+ * contain both upper and lower case letters, at least one number,
+ * and at least one non alphanumeric character.
+ */
+ @XmlElement
+ private String password;
+
+ private byte[] salt;
+
+
+
public UserConfig() {
}
public UserConfig(String user, String password, List<String> roles) {
this.user = user;
- this.password = password;
- if (this.validatePassword().isSuccess()) {
- /*
- * Only if the password is a valid one, hash it. So in case it is not
- * valid, when UserConfig.validate() is called, the proper
- * validation error will be returned to the caller. If we hashed a
- * priori instead, the mis-configuration would be masked
- */
- this.password = hash(this.password);
+ /*
+ * Password validation to be done on clear text password. If fails, mark
+ * the password with a well known label, so that object validation can
+ * report the proper error. Only if password is a valid one, generate
+ * salt, concatenate it with clear text password and hash the
+ * resulting string. Hash result is going to be our stored password.
+ */
+ if (validateClearTextPassword(password).isSuccess()) {
+ this.salt = BitBufferHelper.toByteArray(randomGenerator.nextLong());
+ this.password = hash(salt, password);
+ } else {
+ this.salt = null;
+ this.password = BAD_PASSWORD;
}
- this.roles = (roles == null) ? new ArrayList<String>() : new ArrayList<String>(roles);
+ this.roles = (roles == null) ? Collections.<String>emptyList() : new ArrayList<String>(roles);
}
public String getUser() {
}
public Status validate() {
- Status validCheck = validateRoles();
+ Status validCheck = validateUsername();
if (validCheck.isSuccess()) {
- validCheck = validateUsername();
+ // Password validation was run at object construction time
+ validCheck = (!password.equals(BAD_PASSWORD)) ? new Status(StatusCode.SUCCESS) : new Status(
+ StatusCode.BADREQUEST,
+ "Password should be 8 to 256 characters long, contain both upper and lower case letters, "
+ + "at least one number and at least one non alphanumeric character");
}
if (validCheck.isSuccess()) {
- validCheck = validatePassword();
+ validCheck = validateRoles();
}
return validCheck;
}
return new Status(StatusCode.SUCCESS);
}
- private Status validatePassword() {
+ private Status validateClearTextPassword(String password) {
if (password == null || password.isEmpty()) {
return new Status(StatusCode.BADREQUEST, "Password cannot be empty");
}
- if (password.length() < UserConfig.PASSWORD_MINLENGTH
- || password.length() > UserConfig.PASSWORD_MAXLENGTH) {
- return new Status(StatusCode.BADREQUEST,
- "Password should have 5-256 characters");
+ if (strongPasswordCheck && !password.matches(UserConfig.PASSWORD_REGEX)) {
+ return new Status(StatusCode.BADREQUEST, "Password should be 8 to 256 characters long, "
+ + "contain both upper and lower case letters, at least one number "
+ + "and at least one non alphanumeric character");
}
return new Status(StatusCode.SUCCESS);
}
// To make any changes to a user configured profile, current password
// must always be provided
- if (!this.password.equals(hash(currentPassword))) {
+ if (!this.password.equals(hash(this.salt, currentPassword))) {
return new Status(StatusCode.BADREQUEST, "Current password is incorrect");
}
// Create a new object with the proposed modifications
UserConfig proposed = new UserConfig();
proposed.user = this.user;
- proposed.password = (newPassword == null)? this.password : hash(newPassword);
+ proposed.password = (newPassword == null)? this.password : hash(this.salt, newPassword);
proposed.roles = (newRoles == null)? this.roles : newRoles;
// Validate it
return status;
}
- public AuthResponse authenticate(String clearTextPass) {
+ public AuthResponse authenticate(String clearTextPassword) {
AuthResponse locResponse = new AuthResponse();
- if (password.equals(hash(clearTextPass))) {
+ if (password.equals(hash(this.salt, clearTextPassword))) {
locResponse.setStatus(AuthResultEnum.AUTH_ACCEPT_LOC);
locResponse.addData(getRolesString());
} else {
return buffer.toString();
}
- public static String hash(String message) {
+ private static byte[] concatenate(byte[] salt, String password) {
+ byte[] messageArray = password.getBytes();
+ byte[] concatenation = new byte[salt.length + password.length()];
+ System.arraycopy(salt, 0, concatenation, 0, salt.length);
+ System.arraycopy(messageArray, 0, concatenation, salt.length, messageArray.length);
+ return concatenation;
+ }
+
+ private static String hash(byte[] salt, String message) {
if (message == null) {
+ log.warn("Password hash requested but empty or no password provided");
return message;
}
+ if (salt == null || salt.length == 0) {
+ log.warn("Password hash requested but empty or no salt provided");
+ return message;
+ }
+
+ // Concatenate salt and password
+ byte[] messageArray = message.getBytes();
+ byte[] concatenation = new byte[salt.length + message.length()];
+ System.arraycopy(salt, 0, concatenation, 0, salt.length);
+ System.arraycopy(messageArray, 0, concatenation, salt.length, messageArray.length);
+
UserConfig.oneWayFunction.reset();
- return HexEncode.bytesToHexString(UserConfig.oneWayFunction.digest(message.getBytes(Charset.defaultCharset())));
+ return HexEncode.bytesToHexString(UserConfig.oneWayFunction.digest(concatenate(salt, message)));
+ }
+
+ /**
+ * Returns UserConfig instance populated with the passed parameters. It does
+ * not run any checks on the passed parameters.
+ *
+ * @param userName
+ * the user name
+ * @param password
+ * the plain text password
+ * @param roles
+ * the list of roles
+ * @return the UserConfig object populated with the passed parameters. No
+ * validity check is run on the input parameters.
+ */
+ public static UserConfig getUncheckedUserConfig(String userName, String password, List<String> roles) {
+ UserConfig config = new UserConfig();
+ config.user = userName;
+ config.salt = BitBufferHelper.toByteArray(randomGenerator.nextLong());
+ config.password = hash(config.salt, password);
+ config.roles = roles;
+ return config;
}
}