/**
* The contents of this file are subject to the OpenMRS Public License
* Version 1.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://license.openmrs.org
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* Copyright (C) OpenMRS, LLC. All Rights Reserved.
*/
package org.openmrs.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.api.APIException;
/**
* OpenMRS's security class deals with the hashing of passwords.
*/
public class Security {
public static Log log = LogFactory.getLog("org.openmrs.util.Security");
/**
* Compare the given hash and the given string-to-hash to see if they are equal. The
* string-to-hash is usually of the form password + salt. <br/>
* <br/>
* This should be used so that this class can compare against the new correct hashing algorithm
* and the old incorrect hashin algorithm.
*
* @param hashedPassword a stored password that has been hashed previously
* @param passwordToHash a string to encode/hash and compare to hashedPassword
* @return true/false whether the two are equal
* @since 1.5
* @should match strings hashed with incorrect sha1 algorithm
* @should match strings hashed with sha1 algorithm
* @should match strings hashed with sha512 algorithm and 128 characters salt
*/
public static boolean hashMatches(String hashedPassword, String passwordToHash) {
if (hashedPassword == null || passwordToHash == null)
throw new APIException("Neither the hashed password or the password to hash cannot be null");
return hashedPassword.equals(encodeString(passwordToHash))
|| hashedPassword.equals(encodeStringSHA1(passwordToHash))
|| hashedPassword.equals(incorrectlyEncodeString(passwordToHash));
}
/**
* This method will hash <code>strToEncode</code> using the preferred algorithm. Currently,
* OpenMRS's preferred algorithm is hard coded to be SHA-512.
*
* @param strToEncode string to encode
* @return the SHA-512 encryption of a given string
* @should encode strings to 128 characters
*/
public static String encodeString(String strToEncode) throws APIException {
String algorithm = "SHA-512";
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
}
catch (NoSuchAlgorithmException e) {
// Yikes! Can't encode password...what to do?
log.error("Can't encode password because the given algorithm: " + algorithm + "was not found! (fail)", e);
throw new APIException("System cannot find password encryption algorithm", e);
}
byte[] input = strToEncode.getBytes(); //TODO: pick a specific character encoding, don't rely on the platform default
return hexString(md.digest(input));
}
/**
* This method will hash <code>strToEncode</code> using the old SHA-1 algorithm.
*
* @param strToEncode string to encode
* @return the SHA-1 encryption of a given string
*/
private static String encodeStringSHA1(String strToEncode) throws APIException {
String algorithm = "SHA1";
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
}
catch (NoSuchAlgorithmException e) {
// Yikes! Can't encode password...what to do?
log.error("Can't encode password because the given algorithm: " + algorithm + "was not found! (fail)", e);
throw new APIException("System cannot find SHA1 encryption algorithm", e);
}
byte[] input = strToEncode.getBytes(); //TODO: pick a specific character encoding, don't rely on the platform default
return hexString(md.digest(input));
}
/**
* Convenience method to convert a byte array to a string
*
* @param b Byte array to convert to HexString
* @return Hexidecimal based string
*/
private static String hexString(byte[] block) {
StringBuffer buf = new StringBuffer();
char[] hexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
int len = block.length;
int high = 0;
int low = 0;
for (int i = 0; i < len; i++) {
high = ((block[i] & 0xf0) >> 4);
low = (block[i] & 0x0f);
buf.append(hexChars[high]);
buf.append(hexChars[low]);
}
return buf.toString();
}
/**
* This method will hash <code>strToEncode</code> using SHA-1 and the incorrect hashing method
* that sometimes dropped out leading zeros.
*
* @param strToEncode string to encode
* @return the SHA-1 encryption of a given string
*/
private static String incorrectlyEncodeString(String strToEncode) throws APIException {
String algorithm = "SHA1";
MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
}
catch (NoSuchAlgorithmException e) {
// Yikes! Can't encode password...what to do?
log.error("Can't encode password because the given algorithm: " + algorithm + "was not found! (fail)", e);
throw new APIException("System cannot find SHA1 encryption algorithm", e);
}
byte[] input = strToEncode.getBytes(); //TODO: pick a specific character encoding, don't rely on the platform default
return incorrectHexString(md.digest(input));
}
/**
* This method used to be the simple hexString method, however, as pointed out in ticket
* http://dev.openmrs.org/ticket/1178, it was not working correctly. Authenticated still needs
* to occur against both this method and the correct hex string, so this wrong implementation
* will remain until we either force users to change their passwords, or we just decide to
* invalidate them.
*
* @param b
* @return the old possibly less than 40 characters hashed string
*/
private static String incorrectHexString(byte[] b) {
if (b == null || b.length < 1)
return "";
StringBuffer s = new StringBuffer();
for (int i = 0; i < b.length; i++) {
s.append(Integer.toHexString(b[i] & 0xFF));
}
return new String(s);
}
/**
* This method will generate a random string
*
* @return a secure random token.
*/
public static String getRandomToken() throws APIException {
Random rng = new Random();
return encodeString(Long.toString(System.currentTimeMillis()) + Long.toString(rng.nextLong()));
}
}