/** * 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())); } }