/******************************************************************************* * 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.ofbiz.base.crypto; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.apache.commons.lang.RandomStringUtils; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.GeneralRuntimeException; import org.apache.ofbiz.base.util.StringUtil; import org.apache.ofbiz.base.util.UtilIO; import org.apache.ofbiz.base.util.UtilProperties; import org.apache.ofbiz.base.util.UtilValidate; /** * Utility class for doing SHA-1/MD5/PBKDF2 One-Way Hash Encryption * */ public class HashCrypt { public static final String module = HashCrypt.class.getName(); public static final String CRYPT_CHAR_SET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"; private static final String PBKDF2_SHA1 ="PBKDF2-SHA1"; private static final String PBKDF2_SHA256 ="PBKDF2-SHA256"; private static final String PBKDF2_SHA384 ="PBKDF2-SHA384"; private static final String PBKDF2_SHA512 ="PBKDF2-SHA512"; private static final int PBKDF2_ITERATIONS = UtilProperties.getPropertyAsInteger("security.properties", "password.encrypt.pbkdf2.iterations", 10000); public static MessageDigest getMessageDigest(String type) { try { return MessageDigest.getInstance(type); } catch (NoSuchAlgorithmException e) { throw new GeneralRuntimeException("Could not load digestor(" + type + ")", e); } } public static boolean comparePassword(String crypted, String defaultCrypt, String password) { if (crypted.startsWith("{PBKDF2")) { return doComparePbkdf2(crypted, password); } else if (crypted.startsWith("{")) { // FIXME: should have been getBytes("UTF-8") originally return doCompareTypePrefix(crypted, defaultCrypt, password.getBytes()); } else if (crypted.startsWith("$")) { return doComparePosix(crypted, defaultCrypt, password.getBytes(UtilIO.getUtf8())); } else { // FIXME: should have been getBytes("UTF-8") originally return doCompareBare(crypted, defaultCrypt, password.getBytes()); } } private static boolean doCompareTypePrefix(String crypted, String defaultCrypt, byte[] bytes) { int typeEnd = crypted.indexOf("}"); String hashType = crypted.substring(1, typeEnd); String hashed = crypted.substring(typeEnd + 1); MessageDigest messagedigest = getMessageDigest(hashType); messagedigest.update(bytes); byte[] digestBytes = messagedigest.digest(); char[] digestChars = Hex.encodeHex(digestBytes); String checkCrypted = new String(digestChars); if (hashed.equals(checkCrypted)) { return true; } // This next block should be removed when all {prefix}oldFunnyHex are fixed. if (hashed.equals(oldFunnyHex(digestBytes))) { Debug.logWarning("Warning: detected oldFunnyHex password prefixed with a hashType; this is not valid, please update the value in the database with ({%s}%s)", module, hashType, checkCrypted); return true; } return false; } private static boolean doComparePosix(String crypted, String defaultCrypt, byte[] bytes) { int typeEnd = crypted.indexOf("$", 1); int saltEnd = crypted.indexOf("$", typeEnd + 1); String hashType = crypted.substring(1, typeEnd); String salt = crypted.substring(typeEnd + 1, saltEnd); String hashed = crypted.substring(saltEnd + 1); return hashed.equals(getCryptedBytes(hashType, salt, bytes)); } private static boolean doCompareBare(String crypted, String defaultCrypt, byte[] bytes) { String hashType = defaultCrypt; String hashed = crypted; MessageDigest messagedigest = getMessageDigest(hashType); messagedigest.update(bytes); return hashed.equals(oldFunnyHex(messagedigest.digest())); } /* * @deprecated use cryptBytes(hashType, salt, password); eventually, use * cryptUTF8(hashType, salt, password) after all existing installs are * salt-based. If the call-site of cryptPassword is just used to create a *new* * value, then you can switch to cryptUTF8 directly. */ @Deprecated public static String cryptPassword(String hashType, String salt, String password) { if (hashType.startsWith("PBKDF2")) { return password != null ? pbkdf2HashCrypt(hashType, salt, password) : null; } // FIXME: should have been getBytes("UTF-8") originally return password != null ? cryptBytes(hashType, salt, password.getBytes()) : null; } public static String cryptUTF8(String hashType, String salt, String value) { if (hashType.startsWith("PBKDF2")) { return value != null ? pbkdf2HashCrypt(hashType, salt, value) : null; } return value != null ? cryptBytes(hashType, salt, value.getBytes(UtilIO.getUtf8())) : null; } public static String cryptValue(String hashType, String salt, String value) { if (hashType.startsWith("PBKDF2")) { return value != null ? pbkdf2HashCrypt(hashType, salt, value) : null; } return value != null ? cryptBytes(hashType, salt, value.getBytes()) : null; } public static String cryptBytes(String hashType, String salt, byte[] bytes) { if (hashType == null) { hashType = "SHA"; } if (salt == null) { salt = RandomStringUtils.random(new SecureRandom().nextInt(15) + 1, CRYPT_CHAR_SET); } StringBuilder sb = new StringBuilder(); sb.append("$").append(hashType).append("$").append(salt).append("$"); sb.append(getCryptedBytes(hashType, salt, bytes)); return sb.toString(); } private static String getCryptedBytes(String hashType, String salt, byte[] bytes) { try { MessageDigest messagedigest = MessageDigest.getInstance(hashType); messagedigest.update(salt.getBytes(UtilIO.getUtf8())); messagedigest.update(bytes); return Base64.encodeBase64URLSafeString(messagedigest.digest()).replace('+', '.'); } catch (NoSuchAlgorithmException e) { throw new GeneralRuntimeException("Error while comparing password", e); } } public static String pbkdf2HashCrypt(String hashType, String salt, String value){ char[] chars = value.toCharArray(); if (UtilValidate.isEmpty(salt)) { salt = getSalt(); } try { PBEKeySpec spec = new PBEKeySpec(chars, salt.getBytes(UtilIO.getUtf8()), PBKDF2_ITERATIONS, 64 * 4); SecretKeyFactory skf = SecretKeyFactory.getInstance(hashType); byte[] hash = Base64.encodeBase64(skf.generateSecret(spec).getEncoded()); String pbkdf2Type = null; switch (hashType) { case "PBKDF2WithHmacSHA1": pbkdf2Type = PBKDF2_SHA1; break; case "PBKDF2WithHmacSHA256": pbkdf2Type = PBKDF2_SHA256; break; case "PBKDF2WithHmacSHA384": pbkdf2Type = PBKDF2_SHA384; break; case "PBKDF2WithHmacSHA512": pbkdf2Type = PBKDF2_SHA512; break; default: pbkdf2Type = PBKDF2_SHA1; } StringBuilder sb = new StringBuilder(); sb.append("{").append(pbkdf2Type).append("}"); sb.append(PBKDF2_ITERATIONS).append("$"); sb.append(org.apache.ofbiz.base.util.Base64.base64Encode(salt)).append("$"); sb.append(new String(hash)).toString(); return sb.toString(); } catch (InvalidKeySpecException e) { throw new GeneralRuntimeException("Error while creating SecretKey", e); } catch (NoSuchAlgorithmException e) { throw new GeneralRuntimeException("Error while computing SecretKeyFactory", e); } } public static boolean doComparePbkdf2(String crypted, String password){ try { int typeEnd = crypted.indexOf("}"); String hashType = crypted.substring(1, typeEnd); String[] parts = crypted.split("\\$"); int iterations = Integer.parseInt(parts[0].substring(typeEnd+1)); byte[] salt = org.apache.ofbiz.base.util.Base64.base64Decode(parts[1]).getBytes(); byte[] hash = Base64.decodeBase64(parts[2].getBytes()); PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, hash.length * 8); switch (hashType.substring(hashType.indexOf("-")+1)) { case "SHA256": hashType = "PBKDF2WithHmacSHA256"; break; case "SHA384": hashType = "PBKDF2WithHmacSHA384"; break; case "SHA512": hashType = "PBKDF2WithHmacSHA512"; break; default: hashType = "PBKDF2WithHmacSHA1"; } SecretKeyFactory skf = SecretKeyFactory.getInstance(hashType); byte[] testHash = skf.generateSecret(spec).getEncoded(); int diff = hash.length ^ testHash.length; for (int i = 0; i < hash.length && i < testHash.length; i++) { diff |= hash[i] ^ testHash[i]; } return diff == 0; } catch (NoSuchAlgorithmException e) { throw new GeneralRuntimeException("Error while computing SecretKeyFactory", e); } catch (InvalidKeySpecException e) { throw new GeneralRuntimeException("Error while creating SecretKey", e); } } private static String getSalt() { try { SecureRandom sr = SecureRandom.getInstance("SHA1PRNG"); byte[] salt = new byte[16]; sr.nextBytes(salt); return salt.toString(); } catch (NoSuchAlgorithmException e) { throw new GeneralRuntimeException("Error while creating salt", e); } } /** * @deprecated use digestHash("SHA", null, str) */ @Deprecated public static String getDigestHash(String str) { return digestHash("SHA", null, str); } /** * @deprecated use digestHash(hashType, null, str)) */ @Deprecated public static String getDigestHash(String str, String hashType) { return digestHash(hashType, null, str); } /** * @deprecated use digestHash(hashType, code, str); */ @Deprecated public static String getDigestHash(String str, String code, String hashType) { return digestHash(hashType, code, str); } public static String digestHash(String hashType, String code, String str) { if (str == null) return null; byte[] codeBytes; try { if (code == null) codeBytes = str.getBytes(); else codeBytes = str.getBytes(code); } catch (UnsupportedEncodingException e) { throw new GeneralRuntimeException("Error while computing hash of type " + hashType, e); } return digestHash(hashType, codeBytes); } public static String digestHash(String hashType, byte[] bytes) { try { MessageDigest messagedigest = MessageDigest.getInstance(hashType); messagedigest.update(bytes); byte[] digestBytes = messagedigest.digest(); char[] digestChars = Hex.encodeHex(digestBytes); StringBuilder sb = new StringBuilder(); sb.append("{").append(hashType).append("}"); sb.append(digestChars, 0, digestChars.length); return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new GeneralRuntimeException("Error while computing hash of type " + hashType, e); } } public static String digestHash64(String hashType, byte[] bytes) { if (hashType == null) { hashType = "SHA"; } try { MessageDigest messagedigest = MessageDigest.getInstance(hashType); messagedigest.update(bytes); byte[] digestBytes = messagedigest.digest(); StringBuilder sb = new StringBuilder(); sb.append("{").append(hashType).append("}"); sb.append(Base64.encodeBase64URLSafeString(digestBytes).replace('+', '.')); return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new GeneralRuntimeException("Error while computing hash of type " + hashType, e); } } /** * @deprecated use cryptPassword */ @Deprecated public static String getHashTypeFromPrefix(String hashString) { if (UtilValidate.isEmpty(hashString) || hashString.charAt(0) != '{') { return null; } return hashString.substring(1, hashString.indexOf('}')); } /** * @deprecated use cryptPassword */ @Deprecated public static String removeHashTypePrefix(String hashString) { if (UtilValidate.isEmpty(hashString) || hashString.charAt(0) != '{') { return hashString; } return hashString.substring(hashString.indexOf('}') + 1); } /** * @deprecated use digestHashOldFunnyHex(hashType, str) */ @Deprecated public static String getDigestHashOldFunnyHexEncode(String str, String hashType) { return digestHashOldFunnyHex(hashType, str); } public static String digestHashOldFunnyHex(String hashType, String str) { if (UtilValidate.isEmpty(hashType)) hashType = "SHA"; if (str == null) return null; try { MessageDigest messagedigest = MessageDigest.getInstance(hashType); byte[] strBytes = str.getBytes(); messagedigest.update(strBytes); return oldFunnyHex(messagedigest.digest()); } catch (Exception e) { Debug.logError(e, "Error while computing hash of type " + hashType, module); } return str; } // This next block should be removed when all {prefix}oldFunnyHex are fixed. private static String oldFunnyHex(byte[] bytes) { int k = 0; char[] digestChars = new char[bytes.length * 2]; for (int l = 0; l < bytes.length; l++) { int i1 = bytes[l]; if (i1 < 0) { i1 = 127 + i1 * -1; } StringUtil.encodeInt(i1, k, digestChars); k += 2; } return new String(digestChars); } }