Skip to content

Commit

Permalink
Merge pull request #1708 from lat-lon/feature/improveConsolePwdEncryp…
Browse files Browse the repository at this point in the history
…tion-1616

Changing format of password stored in console.pw to UNIX crypt extended format
  • Loading branch information
stephanr authored Jun 7, 2024
2 parents 6a613f2 + 15616a8 commit 9136802
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 68 deletions.
4 changes: 4 additions & 0 deletions deegree-services/deegree-webservices/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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";

Expand Down Expand Up @@ -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) {
Expand All @@ -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();
}

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <code>PasswordFile</code> class encapsulates a text file storing a
* <code>SaltedPassword</code>.
*
* As of deegree version 3.6 the encoding format of the salted password has been changed
* to <code>$ID$SALT$PWD</code>. In previous versions of deegree the format has been
* <code>SALT:PWD</code>.
*
* <b>Attention:</b> There is no automatic password migration available. Files created
* with older versions of deegree need to be recreated with deegree 3.6.
*
* @author <a href="mailto:schneider@occamlabs.de">Markus Schneider</a>
* @author <a href="mailto:friebe@lat-lon.de">Torsten Friebe</a>
* @since 3.0
* @see SaltedPassword
*/
public class PasswordFile implements Serializable {

private static final long serialVersionUID = -8331316987059763053L;
Expand All @@ -55,7 +73,6 @@ public PasswordFile(File file) {
* @return salted password, never <code>null</code>
*/
public SaltedPassword getCurrentContent() throws IOException {

SaltedPassword saltedPw = null;
if (file.exists()) {
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
Expand All @@ -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();
Expand All @@ -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]);
}

/**
Expand All @@ -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 + "'.");
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
* <pre>
* $ID$SALT$PWD
* </pre>
*
* The ID for the SHA-256 and SHA-512 methods are as follows:
*
* <pre>
* ID | Method
* -------------------------------
* 5 | SHA-256
* 6 | SHA-512
* </pre>
*
* Currently only the SHA-256 method is used.
*
* @author <a href="mailto:schneider@occamlabs.de">Markus Schneider</a>
* @author <a href="mailto:friebe@lat-lon.de">Torsten Friebe</a>
* @version 3.6
* @since 3.3
* @see <a href=
* "https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/digest/Sha2Crypt.html">Apache
* Commons Codec Sha2Crypt</a>
* @see <a href="https://www.akkadia.org/drepper/SHA-crypt.txt">Unix crypt using SHA-256
* and SHA-512</a>
*/
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) {
Expand All @@ -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());
}
}

}
Loading

0 comments on commit 9136802

Please sign in to comment.