/** * 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.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Random; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xerces.impl.dv.util.Base64; import org.openmrs.api.APIException; import org.openmrs.api.context.Context; import org.springframework.util.StringUtils; /** * OpenMRS's security class deals with the hashing of passwords. */ public class Security { /** * Defined encoding to avoid using default platform charset */ private static final String encoding = "UTF-8"; /** * encryption settings */ public static Log log = LogFactory.getLog(Security.class); /** * 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; byte[] input; try { md = MessageDigest.getInstance(algorithm); input = strToEncode.getBytes(encoding); } 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); } catch (UnsupportedEncodingException e) { throw new APIException("System cannot find " + encoding + " encoding", e); } 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; byte[] input; try { md = MessageDigest.getInstance(algorithm); input = strToEncode.getBytes(encoding); } 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); } catch (UnsupportedEncodingException e) { throw new APIException("System cannot find " + encoding + " encoding", e); } 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; byte[] input; try { md = MessageDigest.getInstance(algorithm); input = strToEncode.getBytes(encoding); } 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); } catch (UnsupportedEncodingException e) { throw new APIException("System cannot find " + encoding + " encoding", e); } 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())); } /** * encrypt text to a string with specific initVector and secretKey; rarely used except in * testing and where specifically necessary * * @see #encrypt(String) * * @param text string to be encrypted * @param initVector custom init vector byte array * @param secretKey custom secret key byte array * @return encrypted text * @since 1.9 */ public static String encrypt(String text, byte[] initVector, byte[] secretKey) { IvParameterSpec initVectorSpec = new IvParameterSpec(initVector); SecretKeySpec secret = new SecretKeySpec(secretKey, OpenmrsConstants.ENCRYPTION_KEY_SPEC); byte[] encrypted; try { Cipher cipher = Cipher.getInstance(OpenmrsConstants.ENCRYPTION_CIPHER_CONFIGURATION); cipher.init(Cipher.ENCRYPT_MODE, secret, initVectorSpec); encrypted = cipher.doFinal(text.getBytes(encoding)); } catch (GeneralSecurityException e) { throw new APIException("could not encrypt text", e); } catch (UnsupportedEncodingException e) { throw new APIException("System cannot find " + encoding + " encoding", e); } return Base64.encode(encrypted); } /** * encrypt text using stored initVector and securityKey * * @param text * @return encrypted text * @since 1.9 * @should encrypt short and long text */ public static String encrypt(String text) { return Security.encrypt(text, Security.getSavedInitVector(), Security.getSavedSecretKey()); } /** * decrypt text to a string with specific initVector and secretKey; rarely used except in * testing and where specifically necessary * * @see #decrypt(String) * * @param text text to be decrypted * @param initVector custom init vector byte array * @param secretKey custom secret key byte array * @return decrypted text * @since 1.9 */ public static String decrypt(String text, byte[] initVector, byte[] secretKey) { IvParameterSpec initVectorSpec = new IvParameterSpec(initVector); SecretKeySpec secret = new SecretKeySpec(secretKey, OpenmrsConstants.ENCRYPTION_KEY_SPEC); String decrypted = null; try { Cipher cipher = Cipher.getInstance(OpenmrsConstants.ENCRYPTION_CIPHER_CONFIGURATION); cipher.init(Cipher.DECRYPT_MODE, secret, initVectorSpec); byte[] original = cipher.doFinal(Base64.decode(text)); decrypted = new String(original, encoding); } catch (GeneralSecurityException e) { throw new APIException("could not decrypt text", e); } catch (UnsupportedEncodingException e) { throw new APIException("System cannot find " + encoding + " encoding", e); } return decrypted; } /** * decrypt text using stored initVector and securityKey * * @param text text to be decrypted * @return decrypted text * @since 1.9 * @should decrypt short and long text */ public static String decrypt(String text) { return Security.decrypt(text, Security.getSavedInitVector(), Security.getSavedSecretKey()); } /** * retrieve the stored init vector from runtime properties * * @return stored init vector byte array * @since 1.9 */ public static byte[] getSavedInitVector() { String initVectorText = Context.getRuntimeProperties().getProperty( OpenmrsConstants.ENCRYPTION_VECTOR_RUNTIME_PROPERTY, OpenmrsConstants.ENCRYPTION_VECTOR_DEFAULT); if (StringUtils.hasText(initVectorText)) return Base64.decode(initVectorText); throw new APIException("no encryption initialization vector found"); } /** * generate a new cipher initialization vector; should only be called once in order to not * invalidate all encrypted data * * @return a random array of 16 bytes * @since 1.9 */ public static byte[] generateNewInitVector() { // initialize the init vector with 16 random bytes byte[] initVector = new byte[16]; new SecureRandom().nextBytes(initVector); // TODO get the following (better) method working // Cipher cipher = Cipher.getInstance(CIPHER_CONFIGURATION); // AlgorithmParameters params = cipher.getParameters(); // byte[] initVector = params.getParameterSpec(IvParameterSpec.class).getIV(); return initVector; } /** * retrieve the secret key from runtime properties * * @return stored secret key byte array * @since 1.9 */ public static byte[] getSavedSecretKey() { String keyText = Context.getRuntimeProperties().getProperty(OpenmrsConstants.ENCRYPTION_KEY_RUNTIME_PROPERTY, OpenmrsConstants.ENCRYPTION_KEY_DEFAULT); if (StringUtils.hasText(keyText)) return Base64.decode(keyText); throw new APIException("no encryption secret key found"); } /** * generate a new secret key; should only be called once in order to not invalidate all * encrypted data * * @return generated secret key byte array * @since 1.9 */ public static byte[] generateNewSecretKey() { // Get the KeyGenerator KeyGenerator kgen = null; try { kgen = KeyGenerator.getInstance(OpenmrsConstants.ENCRYPTION_KEY_SPEC); } catch (NoSuchAlgorithmException e) { throw new APIException("Could not generate cipher key", e); } kgen.init(128); // 192 and 256 bits may not be available // Generate the secret key specs. SecretKey skey = kgen.generateKey(); return skey.getEncoded(); } }