org.primefaces
primefaces
diff --git a/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/LogBean.java b/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/LogBean.java
index d1f9588c0d..9912a8850b 100644
--- a/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/LogBean.java
+++ b/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/LogBean.java
@@ -29,6 +29,7 @@
import static jakarta.faces.application.FacesMessage.SEVERITY_ERROR;
import static jakarta.faces.application.FacesMessage.SEVERITY_WARN;
+import static org.slf4j.LoggerFactory.getLogger;
import java.io.File;
import java.io.IOException;
@@ -41,6 +42,7 @@
import jakarta.inject.Named;
import org.deegree.commons.config.DeegreeWorkspace;
+import org.slf4j.Logger;
/**
* JSF backing bean for logging in, logging out, checking login status and password
@@ -56,7 +58,9 @@ public class LogBean implements Serializable {
private static final long serialVersionUID = -4865071415988778817L;
- private static final String PASSWORD_FILE = "console.pw";
+ private static final Logger LOG = getLogger(LogBean.class);
+
+ protected static final String PASSWORD_FILE = "console.pw";
public static final String CONSOLE = "/index";
@@ -109,7 +113,7 @@ public String getNewPassword2() {
return newPassword2;
}
- public String logIn() throws NoSuchAlgorithmException, IOException {
+ public String logIn() throws IOException {
SaltedPassword storedPassword = passwordFile.getCurrentContent();
if (storedPassword == null) {
@@ -121,6 +125,7 @@ public String logIn() throws NoSuchAlgorithmException, IOException {
SaltedPassword givenPassword = new SaltedPassword(currentPassword, storedPassword.getSalt());
loggedIn = storedPassword.equals(givenPassword);
+ LOG.debug("Provided password matches stored password: {}", loggedIn);
return FacesContext.getCurrentInstance().getViewRoot().getViewId();
}
@@ -145,9 +150,10 @@ public String changePassword() throws NoSuchAlgorithmException, IOException {
SaltedPassword newSaltedPassword = new SaltedPassword(newPassword);
passwordFile.update(newSaltedPassword);
+ LOG.info("Password file updated successfully");
}
catch (Throwable e) {
- e.printStackTrace();
+ LOG.error("Failed to update password file due to {}", e.getMessage(), e);
FacesMessage fm = new FacesMessage(SEVERITY_ERROR, "Error updating password: " + e.getMessage(), null);
FacesContext.getCurrentInstance().addMessage(null, fm);
return CHANGE_PASSWORD;
diff --git a/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/PasswordFile.java b/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/PasswordFile.java
index 39b44ff7d0..6f2adffd54 100644
--- a/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/PasswordFile.java
+++ b/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/PasswordFile.java
@@ -28,6 +28,7 @@
package org.deegree.console.security;
import static java.lang.Character.digit;
+import static org.deegree.console.security.SaltedPassword.SHA256_PREFIX;
import java.io.BufferedReader;
import java.io.File;
@@ -36,10 +37,27 @@
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.commons.io.IOUtils;
+/**
+ * An instance of the PasswordFile
class encapsulates a text file storing a
+ * SaltedPassword
.
+ *
+ * As of deegree version 3.6 the encoding format of the salted password has been changed
+ * to $ID$SALT$PWD
. In previous versions of deegree the format has been
+ * SALT:PWD
.
+ *
+ * Attention: There is no automatic password migration available. Files created
+ * with older versions of deegree need to be recreated with deegree 3.6.
+ *
+ * @author Markus Schneider
+ * @author Torsten Friebe
+ * @since 3.0
+ * @see SaltedPassword
+ */
public class PasswordFile implements Serializable {
private static final long serialVersionUID = -8331316987059763053L;
@@ -55,7 +73,6 @@ public PasswordFile(File file) {
* @return salted password, never null
*/
public SaltedPassword getCurrentContent() throws IOException {
-
SaltedPassword saltedPw = null;
if (file.exists()) {
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
@@ -64,7 +81,7 @@ public SaltedPassword getCurrentContent() throws IOException {
if (lines.size() != 1) {
throw new IOException("Password file has incorrect format.");
}
- saltedPw = decodeHexEncodedSaltAndPassword(lines.get(0));
+ saltedPw = parseSaltedPassword(lines.get(0));
}
finally {
in.close();
@@ -73,26 +90,14 @@ public SaltedPassword getCurrentContent() throws IOException {
return saltedPw;
}
- private SaltedPassword decodeHexEncodedSaltAndPassword(String encoded) throws IOException {
-
- int offset = encoded.indexOf(':');
+ private SaltedPassword parseSaltedPassword(String encoded) throws IOException {
+ int offset = encoded.indexOf('$');
if (offset == -1) {
throw new IOException("Password file has incorrect format.");
}
- String hexEncodedSalt = encoded.substring(0, offset);
- String hexEncodedSaltedAndHashedPassword = encoded.substring(offset + 1, encoded.length());
- byte[] salt = decodeHexString(hexEncodedSalt);
- byte[] saltedAndHashedPassword = decodeHexString(hexEncodedSaltedAndHashedPassword);
- return new SaltedPassword(saltedAndHashedPassword, salt);
- }
+ String[] parts = encoded.split("\\$");
- private byte[] decodeHexString(String s) {
- int len = s.length();
- byte[] data = new byte[len / 2];
- for (int i = 0; i < len; i += 2) {
- data[i / 2] = (byte) ((digit(s.charAt(i), 16) << 4) + digit(s.charAt(i + 1), 16));
- }
- return data;
+ return new SaltedPassword(parts[3].getBytes(StandardCharsets.UTF_8), SHA256_PREFIX + parts[2]);
}
/**
@@ -101,7 +106,6 @@ private byte[] decodeHexString(String s) {
* @throws IOException if the password could not be stored
*/
public void update(SaltedPassword pw) throws IOException {
-
if (file.exists()) {
if (!file.delete()) {
throw new IOException("Could not delete existing password file '" + file + "'.");
@@ -112,25 +116,13 @@ public void update(SaltedPassword pw) throws IOException {
file.getParentFile().mkdirs();
}
- PrintWriter writer = new PrintWriter(file, "UTF-8");
- writer.print(encodeHexString(pw.getSalt()));
- writer.print(':');
- writer.println(encodeHexString(pw.getSaltedAndHashedPassword()));
- writer.close();
+ writePasswordToWriter(new PrintWriter(file, StandardCharsets.UTF_8), pw);
}
- private String encodeHexString(final byte[] bytes) {
- if (bytes == null) {
- return null;
- }
- final StringBuilder hex = new StringBuilder(2 * bytes.length);
- for (final byte b : bytes) {
- final int hiVal = (b & 0xF0) >> 4;
- final int loVal = b & 0x0F;
- hex.append((char) ('0' + (hiVal + (hiVal / 10 * 7))));
- hex.append((char) ('0' + (loVal + (loVal / 10 * 7))));
- }
- return hex.toString();
+ protected void writePasswordToWriter(PrintWriter writer, SaltedPassword pw) {
+ writer.print(pw.toString());
+ writer.flush();
+ writer.close();
}
public boolean exists() {
diff --git a/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/SaltedPassword.java b/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/SaltedPassword.java
index 4432903d9b..f89d9b34bd 100644
--- a/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/SaltedPassword.java
+++ b/deegree-services/deegree-webservices/src/main/java/org/deegree/console/security/SaltedPassword.java
@@ -34,61 +34,89 @@
----------------------------------------------------------------------------*/
package org.deegree.console.security;
+import org.apache.commons.codec.digest.Sha2Crypt;
+
import java.io.UnsupportedEncodingException;
-import java.nio.ByteBuffer;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Date;
+import java.nio.charset.StandardCharsets;
/**
* Encapsulates a salt value and the hash of a password that has been salted with the same
- * value.
+ * value using Apache Commons Codec SHA-256 implementation.
+ *
+ * As of deegree version 3.6 the encoding format has been changed to the format of the
+ * extended password format as used in UNIX crypt:
+ *
+ *
+ * $ID$SALT$PWD
+ *
+ *
+ * The ID for the SHA-256 and SHA-512 methods are as follows:
+ *
+ *
+ * ID | Method
+ * -------------------------------
+ * 5 | SHA-256
+ * 6 | SHA-512
+ *
+ *
+ * Currently only the SHA-256 method is used.
*
* @author Markus Schneider
+ * @author Torsten Friebe
+ * @version 3.6
+ * @since 3.3
+ * @see Apache
+ * Commons Codec Sha2Crypt
+ * @see Unix crypt using SHA-256
+ * and SHA-512
*/
-public class SaltedPassword {
+public final class SaltedPassword {
- private static final String HASH_ALGORITHM_ID = "SHA-256";
+ private static final String CHARSET = StandardCharsets.UTF_8.toString();
- private static final String CHARSET = "UTF-8";
+ static final String SHA256_PREFIX = "$5$";
private final byte[] saltedAndHashedPassword;
- private final byte[] salt;
+ private final String salt;
- public SaltedPassword(byte[] saltedAndHashedPassword, byte[] salt) {
+ public SaltedPassword(byte[] saltedAndHashedPassword, String salt) {
this.saltedAndHashedPassword = saltedAndHashedPassword;
this.salt = salt;
}
- public SaltedPassword(String plainPassword, byte[] salt)
- throws UnsupportedEncodingException, NoSuchAlgorithmException {
+ public SaltedPassword(String plainPassword, String salt) throws UnsupportedEncodingException {
byte[] plainPasswordBinary = plainPassword.getBytes(CHARSET);
- byte[] saltedAndHashedPassword = getHashedAndSaltedPassword(plainPasswordBinary, salt);
- this.saltedAndHashedPassword = saltedAndHashedPassword;
+ String saltedPassword = Sha2Crypt.sha256Crypt(plainPasswordBinary, salt);
+ int delimiterPos = nthIndexOf(saltedPassword, "$", 3);
+ this.saltedAndHashedPassword = saltedPassword.substring(delimiterPos + 1, saltedPassword.length())
+ .getBytes(StandardCharsets.UTF_8);
this.salt = salt;
}
- public SaltedPassword(String plainPassword) throws UnsupportedEncodingException, NoSuchAlgorithmException {
- this(plainPassword, getNewSalt());
- }
-
- private byte[] getHashedAndSaltedPassword(byte[] plainPassword, byte[] salt)
- throws NoSuchAlgorithmException, UnsupportedEncodingException {
- MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM_ID);
- md.update(plainPassword);
- md.update(salt);
- return md.digest();
+ public SaltedPassword(String plainPassword) throws UnsupportedEncodingException {
+ byte[] plainPasswordBinary = plainPassword.getBytes(CHARSET);
+ String saltedPassword = Sha2Crypt.sha256Crypt(plainPasswordBinary);
+ int delimiterPos = nthIndexOf(saltedPassword, "$", 3);
+ this.salt = saltedPassword.substring(0, delimiterPos);
+ this.saltedAndHashedPassword = saltedPassword.substring(delimiterPos + 1, saltedPassword.length())
+ .getBytes(StandardCharsets.UTF_8);
}
public byte[] getSaltedAndHashedPassword() {
return saltedAndHashedPassword;
}
- public byte[] getSalt() {
+ public String getSalt() {
return salt;
}
+ @Override
+ public String toString() {
+ return this.getSalt() + "$" + new String(this.getSaltedAndHashedPassword(), StandardCharsets.UTF_8);
+ }
+
@Override
public boolean equals(Object o) {
if (o == null) {
@@ -113,10 +141,13 @@ private boolean equalsBytewise(byte[] bytes1, byte[] bytes2) {
return true;
}
- private static byte[] getNewSalt() {
- ByteBuffer byteBuffer = ByteBuffer.allocate(Long.SIZE / 8);
- byteBuffer.putLong(new Date().getTime());
- return byteBuffer.array();
+ private int nthIndexOf(String input, String substring, int nth) {
+ if (nth == 1) {
+ return input.indexOf(substring);
+ }
+ else {
+ return input.indexOf(substring, nthIndexOf(input, substring, nth - 1) + substring.length());
+ }
}
}
diff --git a/deegree-services/deegree-webservices/src/test/java/org/deegree/console/security/PasswordFileTest.java b/deegree-services/deegree-webservices/src/test/java/org/deegree/console/security/PasswordFileTest.java
new file mode 100644
index 0000000000..c0a1c1ee97
--- /dev/null
+++ b/deegree-services/deegree-webservices/src/test/java/org/deegree/console/security/PasswordFileTest.java
@@ -0,0 +1,79 @@
+package org.deegree.console.security;
+
+import static org.deegree.console.security.LogBean.PASSWORD_FILE;
+import static org.deegree.console.security.SaltedPassword.SHA256_PREFIX;
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.isEmptyOrNullString;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.security.NoSuchAlgorithmException;
+
+import org.deegree.commons.config.DeegreeWorkspace;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * @author Torsten Friebe
+ * @since 3.6
+ */
+public class PasswordFileTest {
+
+ /**
+ * This unit tests can validate if the existing console.pw file contains a password in
+ * the new extended password format introduced with deegree version 3.6.
+ * @throws IOException in case of errors
+ */
+ @Test
+ @Ignore
+ public void getCurrentContentFromWorkspaceRoot() throws IOException {
+ File workspaceDir = new File(DeegreeWorkspace.getWorkspaceRoot());
+ File passwordFileFQN = new File(workspaceDir, PASSWORD_FILE);
+ assertTrue(passwordFileFQN.exists());
+ assertTrue(passwordFileFQN.isFile());
+ PasswordFile passwordFile = new PasswordFile(passwordFileFQN);
+ assertTrue(passwordFile.exists());
+ SaltedPassword saltedPassword = passwordFile.getCurrentContent();
+ assertThat(saltedPassword.toString(), not(isEmptyOrNullString()));
+ assertThat(saltedPassword.getSalt(), startsWith(SHA256_PREFIX));
+ }
+
+ /**
+ * Attention: Enabling this unit test will overwrite the content of the console.pw
+ * file! This test is intended as an example how to create a valid console.pw file
+ * with the new extended passwort format introduced with deegree version 3.6.
+ * @throws IOException in case of errors
+ * @since 3.6
+ * @see SaltedPassword
+ */
+ @Test
+ @Ignore
+ public void update() throws IOException {
+ File workspaceDir = new File(DeegreeWorkspace.getWorkspaceRoot());
+ File passwordFileFQN = new File(workspaceDir, PASSWORD_FILE);
+ PasswordFile passwordFile = new PasswordFile(passwordFileFQN);
+ passwordFile.update(new SaltedPassword("deegree3"));
+ }
+
+ @Test
+ public void writePasswordToWriter() throws IOException, NoSuchAlgorithmException {
+ SaltedPassword mypassword = new SaltedPassword("deegree3");
+ PasswordFile passwordFile = new PasswordFile(File.createTempFile("pwd", ".tmp"));
+ StringWriter writer = new StringWriter();
+ passwordFile.writePasswordToWriter(new PrintWriter(writer), mypassword);
+ assertThat(writer.toString(), not(isEmptyOrNullString()));
+ assertThat(writer.toString(), is(equalTo(mypassword.toString())));
+ }
+
+ @Test
+ public void exists() throws IOException {
+ PasswordFile passwordFile = new PasswordFile(File.createTempFile("pwd", ".tmp"));
+ assertTrue(passwordFile.exists());
+ }
+
+}
\ No newline at end of file
diff --git a/deegree-services/deegree-webservices/src/test/java/org/deegree/console/security/SaltedPasswordTest.java b/deegree-services/deegree-webservices/src/test/java/org/deegree/console/security/SaltedPasswordTest.java
new file mode 100644
index 0000000000..a21ff84098
--- /dev/null
+++ b/deegree-services/deegree-webservices/src/test/java/org/deegree/console/security/SaltedPasswordTest.java
@@ -0,0 +1,60 @@
+package org.deegree.console.security;
+
+import org.junit.Test;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+
+import static org.deegree.console.security.SaltedPassword.SHA256_PREFIX;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Torsten Friebe
+ * @since 3.6
+ */
+public class SaltedPasswordTest {
+
+ @Test
+ public void testCreatingFromPlainPassword() throws UnsupportedEncodingException {
+ SaltedPassword plainpassword = new SaltedPassword("foo");
+ String saltedPassword = new String(plainpassword.getSaltedAndHashedPassword(), StandardCharsets.UTF_8);
+ assertThat(plainpassword.getSalt(), startsWith(SHA256_PREFIX));
+ assertThat(saltedPassword, is(notNullValue()));
+ }
+
+ @Test
+ public void testCreatingFromSaltedPassword() {
+ String password = "nxIKX54gpaik7RiymYmMEhDou8.9DjFTzFkJxHKQ3D/";
+ String salt = "$5$12345678";
+ SaltedPassword saltedpassword = new SaltedPassword(password.getBytes(StandardCharsets.UTF_8), salt);
+ assertThat(new String(saltedpassword.getSaltedAndHashedPassword(), StandardCharsets.UTF_8), is(password));
+ assertThat(saltedpassword.getSalt(), is(salt));
+ }
+
+ @Test
+ public void testCreateNewPasswordWithSaltFromOtherPassword() throws UnsupportedEncodingException {
+ String plainpassword = "foo";
+ SaltedPassword storedPassword = new SaltedPassword(plainpassword);
+ String saltFromStoredPassword = storedPassword.getSalt();
+ SaltedPassword givenPassword = new SaltedPassword(plainpassword, saltFromStoredPassword);
+ assertTrue(storedPassword.equals(givenPassword));
+ }
+
+ @Test
+ public void testSaltedPasswordAsParts() throws UnsupportedEncodingException {
+ SaltedPassword password = new SaltedPassword("foo");
+ assertThat(password.toString(), startsWith("$"));
+ String[] parts = password.toString().split("\\$");
+ // first is empty as the string starts with $
+ assertThat(parts[0], is(""));
+ // the ID for SHA-256
+ assertThat(parts[1], is("5"));
+ // the salt
+ assertThat(parts[2], hasLength(8));
+ // the hashed password
+ assertThat(parts[3], hasLength(43));
+ }
+
+}
\ No newline at end of file