Author: cziegeler Date: Tue Nov 13 10:38:33 2018 New Revision: 1846501 URL: http://svn.apache.org/viewvc?rev=1846501&view=rev Log: FELIX-5934 : The Felix Web Console stores unsalted hashed password. Apply patch from Antonio Sanso
Added: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/ felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java (with props) Modified: felix/trunk/webconsole/changelog.txt felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java Modified: felix/trunk/webconsole/changelog.txt URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/changelog.txt?rev=1846501&r1=1846500&r2=1846501&view=diff ============================================================================== --- felix/trunk/webconsole/changelog.txt (original) +++ felix/trunk/webconsole/changelog.txt Tue Nov 13 10:38:33 2018 @@ -1,9 +1,15 @@ +Changes in 4.3.10 +----------------- +** Improvement + * [FELIX-5934] - The web console stores unsalted hashed password + + Changes in 4.3.8 ---------------- ** Improvement - * [5901] - Update to latest jQuery UI 1.12.1 + * [FELIX-5901] - Update to latest jQuery UI 1.12.1 ** Bug - * [5893] - JQuery Security bug CVE-2015-9251 in Web Console + * [FELIX-5893] - JQuery Security bug CVE-2015-9251 in Web Console Changes from 4.3.2 to 4.3.4 Modified: felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java?rev=1846501&r1=1846500&r2=1846501&view=diff ============================================================================== --- felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java (original) +++ felix/trunk/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java Tue Nov 13 10:38:33 2018 @@ -19,10 +19,10 @@ package org.apache.felix.webconsole.internal.servlet; +import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - +import java.security.SecureRandom; /** * The <code>Password</code> class encapsulates encoding and decoding @@ -31,22 +31,29 @@ import java.util.Arrays; * Encoded hashed passwords are strings of the form * <code>{hashAlgorithm}base64-encoded-password-hash</code> where * <i>hashAlgorithm</i> is the name of the hash algorithm used to hash - * the password and <i>base64-encoded-password-hash</i> is the password - * hashed with the indicated hash algorithm and subsequently encoded in - * Base64. + * the password and <i>password</i> is the password + * hashed with the indicated hash algorithm. */ class Password { // the default hash algorithm (part of the Java Platform since 1.4) private static final String DEFAULT_HASH_ALGO = "SHA-256"; + + private static final char DELIMITER = '-'; + + private static final int NO_ITERATIONS = 1; + + private static final int DEFAULT_ITERATIONS = 1000; + + public static final int DEFAULT_SALT_SIZE = 8; // the hash algorithm used to hash the password or null // if the password is not hashed at all private final String hashAlgo; // the hashed or plain password - private final byte[] password; + private final String password; /** @@ -73,18 +80,16 @@ class Password */ static String hashPassword( final String textPassword ) { - final byte[] bytePassword = Base64.getBytesUtf8( textPassword ); - return hashPassword( DEFAULT_HASH_ALGO, bytePassword ); + String salt = generateSalt(DEFAULT_SALT_SIZE); + return hashPassword( DEFAULT_HASH_ALGO, DEFAULT_ITERATIONS, salt, textPassword ); } - Password( String textPassword ) { this.hashAlgo = getPasswordHashAlgorithm( textPassword ); - this.password = getPasswordBytes( textPassword ); + this.password = getPassword(textPassword); } - /** * Returns {@code true} if this password matches the password * {@code toCompare}. If this password is hashed, the {@code toCompare} @@ -97,32 +102,47 @@ class Password */ boolean matches( final byte[] toCompare ) { - return Arrays.equals( this.password, hashPassword( toCompare, this.hashAlgo ) ); + if (this.hashAlgo != null) + { + int startPos = 0; + String salt = extractSalt(this.password, startPos); + int iterations = NO_ITERATIONS; + if (salt != null) + { + startPos += salt.length()+1; + iterations = extractIterations(this.password, startPos); + + } + String hash = hashPassword(this.hashAlgo, iterations, salt, new String(toCompare)); + final StringBuilder buf = new StringBuilder(); + return compareSecure(buf.append("{").append(this.hashAlgo).append("}").append(password).toString(), hash); + } else { + return compareSecure(password, new String(toCompare)); + } + } - - - /** - * Returns this password as a string hashed and encoded as described - * by the class comment. If this password has not been hashed originally, - * the default hash algorithm <i>SHA-256</i> is applied. - */ - public String toString() + + private static String hashPassword( final String hashAlgorithm, final int iterations, final String salt, final String password ) { - return hashPassword( this.hashAlgo, this.password ); - } - + + final StringBuilder buf = new StringBuilder(); + buf.append( '{' ).append( hashAlgorithm.toLowerCase() ).append( '}' ); + if (salt != null && !salt.isEmpty()) { + buf.append(salt).append(DELIMITER); + if (iterations > NO_ITERATIONS) { + buf.append(iterations).append(DELIMITER); + } + final byte[] hashedPassword = hashPassword( password, salt,iterations, hashAlgorithm ); + buf.append( Base64.newStringUtf8( Base64.encodeBase64( hashedPassword ) ) ); + } else { + // backwards compatible to previous version: no salt, no iterations + final byte[] hashedPassword = hashPassword( password, null, NO_ITERATIONS, hashAlgorithm ); + buf.append( Base64.newStringUtf8( Base64.encodeBase64( hashedPassword ) ) ); + } - private static String hashPassword( final String hashAlgorithm, final byte[] password ) - { - final String actualHashAlgo = ( hashAlgorithm == null ) ? DEFAULT_HASH_ALGO : hashAlgorithm; - final byte[] hashedPassword = hashPassword( password, actualHashAlgo ); - final StringBuffer buf = new StringBuffer( 2 + actualHashAlgo.length() + hashedPassword.length * 3 ); - buf.append( '{' ).append( actualHashAlgo.toLowerCase() ).append( '}' ); - buf.append( Base64.newStringUtf8( Base64.encodeBase64( hashedPassword ) ) ); return buf.toString(); } - private static String getPasswordHashAlgorithm( final String textPassword ) { final int endHash = getEndOfHashAlgorithm( textPassword ); @@ -135,17 +155,16 @@ class Password return null; } - - private static byte[] getPasswordBytes( final String textPassword ) + private static String getPassword( final String textPassword ) { final int endHash = getEndOfHashAlgorithm( textPassword ); if ( endHash >= 0 ) { final String encodedPassword = textPassword.substring( endHash + 1 ); - return Base64.decodeBase64( encodedPassword ); + return encodedPassword; } - return Base64.getBytesUtf8( textPassword ); + return textPassword; } @@ -163,23 +182,108 @@ class Password return -1; } - - private static byte[] hashPassword( final byte[] pwd, final String hashAlg ) + private static byte[] hashPassword( final String pwd, final String salt, final int iterations, final String hashAlg ) { - // no hashing if no hash algorithm - if ( hashAlg == null || hashAlg.length() == 0 ) - { - return pwd; - } - try { + StringBuilder data = new StringBuilder(); + if (salt != null) + { + data.append(salt); + } + data.append(pwd); + byte[] bytes = Base64.getBytesUtf8( data.toString()); final MessageDigest md = MessageDigest.getInstance( hashAlg ); - return md.digest( pwd ); + for (int i = 0; i < iterations; i++) + { + md.reset(); + bytes = md.digest(bytes); + } + return bytes; } catch ( NoSuchAlgorithmException e ) { throw new IllegalStateException( "Cannot hash the password: " + e ); } } + + private static boolean compareSecure( final String a,final String b ) + { + int len = a.length(); + if (len != b.length()) + { + return false; + } + if (len == 0) + { + return true; + } + // don't use conditional operations inside the loop + int bits = 0; + for (int i = 0; i < len; i++) + { + // this will never reset any bits + bits |= a.charAt(i) ^ b.charAt(i); + } + return bits == 0; + } + + private static String generateSalt( final int saltSize ) + { + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[saltSize]; + random.nextBytes(salt); + return toHex(salt); + } + + private static String toHex( final byte[] array ) + { + BigInteger bi = new BigInteger(1, array); + String hex = bi.toString(16); + int paddingLength = (array.length * 2) - hex.length(); + if(paddingLength > 0) + { + return String.format("%0" + paddingLength + "d", 0) + hex; + } + else + { + return hex; + } + } + + private static String extractSalt( final String hashedPwd, final int start ) + { + if (hashedPwd != null) + { + int end = hashedPwd.indexOf(DELIMITER, start); + if (end > -1) + { + return hashedPwd.substring(start, end); + } + } + // no salt + return null; + } + + private static int extractIterations( final String hashedPwd, int start ) + { + if (hashedPwd != null) + { + int end = hashedPwd.indexOf(DELIMITER, start); + if (end > -1) + { + String str = hashedPwd.substring(start, end); + try + { + return Integer.parseInt(str); + } catch (NumberFormatException e) + { + //nothing to do + } + } + } + // no extra iterations + return NO_ITERATIONS; + } + } Added: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java URL: http://svn.apache.org/viewvc/felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java?rev=1846501&view=auto ============================================================================== --- felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java (added) +++ felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java Tue Nov 13 10:38:33 2018 @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.felix.webconsole.internal.servlet; + +import org.junit.Assert; +import org.junit.Test; + +public class PasswordTest { + static final String PASSWORD_HASHED = "{sha-256}jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="; + + private Password password; + + @Test + public void test_matches() throws NoSuchFieldException, SecurityException { + password = new Password("{sha-256}ffe2c845abea5d1c-1000-fn7QYN5RT4T2wM2+WJAnPHoZERW9dheoxUam1KW3oEA="); + Assert.assertTrue(password.matches("admin".getBytes())); + //test backward compaibility + password = new Password("password"); + Assert.assertTrue(password.matches("password".getBytes())); + password = new Password("{sha-256}jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="); + Assert.assertTrue(password.matches("admin".getBytes())); + + } + + @Test + public void test_isPasswordHashed() { + Assert.assertTrue(Password.isPasswordHashed(PASSWORD_HASHED)); + Assert.assertFalse(Password.isPasswordHashed("foo")); + } + + @Test + public void test_hashPassword() { + String pwd1 = Password.hashPassword("admin"); + String pwd2 = Password.hashPassword("admin"); + Assert.assertFalse(pwd1.equals(pwd2)); + } + +} Propchange: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java ------------------------------------------------------------------------------ svn:executable = * Propchange: felix/trunk/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/PasswordTest.java ------------------------------------------------------------------------------ svn:keywords = author date id revision rev url