blob: 7011b926ad0c027b6664ac85c91fe32874581328 [file] [log] [blame]
/*
* 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 java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
/**
* The <code>Password</code> class encapsulates encoding and decoding
* operations on plain text and hashed passwords.
* <p>
* 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>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 String password;
/**
* Returns {@code true} if the given {@code textPassword} is hashed
* and encoded as described in the class comment.
*
* @param textPassword
* @return
* @throws NullPointerException if {@code textPassword} is {@code null}.
*/
static boolean isPasswordHashed( final String textPassword )
{
return getEndOfHashAlgorithm( textPassword ) >= 0;
}
/**
* Returns the given plain {@code textPassword} as an encoded hashed
* password string as described in the class comment.
*
* @param textPassword
* @return
* @throws NullPointerException if {@code textPassword} is {@code null}.
*/
static String hashPassword( final String textPassword )
{
String salt = generateSalt(DEFAULT_SALT_SIZE);
return hashPassword( DEFAULT_HASH_ALGO, DEFAULT_ITERATIONS, salt, textPassword );
}
Password( String textPassword )
{
this.hashAlgo = getPasswordHashAlgorithm( textPassword );
this.password = getPassword(textPassword);
}
/**
* Returns {@code true} if this password matches the password
* {@code toCompare}. If this password is hashed, the {@code toCompare}
* password is hashed, too, with the same hash algorithm before
* comparison.
*
* @param toCompare
* @return
* @throws NullPointerException if {@code toCompare} is {@code null}.
*/
boolean matches( final byte[] toCompare )
{
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));
}
}
private static String hashPassword( final String hashAlgorithm, final int iterations, final String salt, final String 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 ) ) );
}
return buf.toString();
}
private static String getPasswordHashAlgorithm( final String textPassword )
{
final int endHash = getEndOfHashAlgorithm( textPassword );
if ( endHash >= 0 )
{
return textPassword.substring( 1, endHash );
}
// password is plain text, hence no algorithm
return null;
}
private static String getPassword( final String textPassword )
{
final int endHash = getEndOfHashAlgorithm( textPassword );
if ( endHash >= 0 )
{
final String encodedPassword = textPassword.substring( endHash + 1 );
return encodedPassword;
}
return textPassword;
}
private static int getEndOfHashAlgorithm( final String textPassword )
{
if ( textPassword.startsWith( "{" ) )
{
final int endHash = textPassword.indexOf( "}" );
if ( endHash > 0 )
{
return endHash;
}
}
return -1;
}
private static byte[] hashPassword( final String pwd, final String salt, final int iterations, final String hashAlg )
{
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 );
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;
}
}