/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.util;
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.xerces.impl.dv.util.Base64;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 Logger log = LoggerFactory.getLogger(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 hashing 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("password.cannot.be.null", (Object[]) null);
}
return hashedPassword.equals(encodeString(passwordToHash))
|| hashedPassword.equals(encodeStringSHA1(passwordToHash))
|| hashedPassword.equals(incorrectlyEncodeString(passwordToHash));
}
/**
* Gets the error message for failing to encode password when the given algorithm was not found
* @param algo algorithm used for encoding
* @return the error message string with algorithm type used
*/
private static String getPasswordEncodeFailMessage(String algo) {
String errorMessage = "Can't encode password because the given algorithm: " + algo + " was not found! (fail)";
return errorMessage;
}
/**
/**
* 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(getPasswordEncodeFailMessage(algorithm), e);
throw new APIException("system.cannot.find.password.encryption.algorithm", null, e);
}
catch (UnsupportedEncodingException e) {
throw new APIException("system.cannot.find.encoding", new Object[] { 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(getPasswordEncodeFailMessage(algorithm), e);
throw new APIException("system.cannot.find.encryption.algorithm", null, e);
}
catch (UnsupportedEncodingException e) {
throw new APIException("system.cannot.find.encoding", new Object[] { 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) {
StringBuilder buf = new StringBuilder();
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(getPasswordEncodeFailMessage(algorithm), e);
throw new APIException("system.cannot.find.encryption.algorithm", null, e);
}
catch (UnsupportedEncodingException e) {
throw new APIException("system.cannot.find.encoding", new Object[] { 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 "";
}
StringBuilder s = new StringBuilder();
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", null, e);
}
catch (UnsupportedEncodingException e) {
throw new APIException("system.cannot.find.encoding", new Object[] { 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", null, e);
}
catch (UnsupportedEncodingException e) {
throw new APIException("system.cannot.find.encoding", new Object[] { 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", (Object[]) null);
}
/**
* 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", (Object[]) null);
}
/**
* 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", null, e);
}
kgen.init(128); // 192 and 256 bits may not be available
// Generate the secret key specs.
SecretKey skey = kgen.generateKey();
return skey.getEncoded();
}
}