blob: 816a4ecb7b0876fa4bbd1b4b762ac088b84b64b6 [file] [log] [blame]
/*
* 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.entity.util;
import java.io.IOException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.codec.binary.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.crypto.OperationMode;
import org.apache.shiro.crypto.hash.DefaultHashService;
import org.apache.shiro.crypto.hash.HashRequest;
import org.apache.shiro.crypto.hash.HashService;
import org.apache.ofbiz.base.crypto.DesCrypt;
import org.apache.ofbiz.base.crypto.HashCrypt;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.GeneralException;
import org.apache.ofbiz.base.util.StringUtil;
import org.apache.ofbiz.base.util.UtilObject;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.EntityCryptoException;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.model.ModelField.EncryptMethod;
import org.apache.ofbiz.entity.transaction.TransactionUtil;
public final class EntityCrypto {
public static final String module = EntityCrypto.class.getName();
protected final Delegator delegator;
protected final ConcurrentMap<String, byte[]> keyMap = new ConcurrentHashMap<String, byte[]>();
protected final StorageHandler[] handlers;
public EntityCrypto(Delegator delegator, String kekText) throws EntityCryptoException {
this.delegator = delegator;
byte[] kek;
kek = UtilValidate.isNotEmpty(kekText) ? Base64.decodeBase64(kekText) : null;
handlers = new StorageHandler[] {
new ShiroStorageHandler(kek),
new SaltedBase64StorageHandler(kek),
NormalHashStorageHandler,
OldFunnyHashStorageHandler,
};
}
public void clearKeyCache() {
keyMap.clear();
}
/** Encrypts an Object into an encrypted hex encoded String */
@Deprecated
public String encrypt(String keyName, Object obj) throws EntityCryptoException {
return encrypt(keyName, EncryptMethod.TRUE, obj);
}
/** Encrypts an Object into an encrypted hex encoded String */
public String encrypt(String keyName, EncryptMethod encryptMethod, Object obj) throws EntityCryptoException {
try {
byte[] key = this.findKey(keyName, handlers[0]);
if (key == null) {
EntityCryptoException caught = null;
try {
this.createKey(keyName, handlers[0], encryptMethod);
} catch (EntityCryptoException e) {
// either a database read error, or a duplicate key insert
// if the latter, try to fetch the value created by the
// other thread.
caught = e;
} finally {
try {
key = this.findKey(keyName, handlers[0]);
} catch (EntityCryptoException e) {
// this is bad, couldn't lookup the value, some bad juju
// is occurring; rethrow the original exception if available
throw caught != null ? caught : e;
}
if (key == null) {
// this is also bad, couldn't find any key
throw caught != null ? caught : new EntityCryptoException("could not lookup key (" + keyName + ") after creation");
}
}
}
return handlers[0].encryptValue(encryptMethod, key, UtilObject.getBytes(obj));
} catch (GeneralException e) {
throw new EntityCryptoException(e);
}
}
// NOTE: this is definitely for debugging purposes only, do not uncomment in production server for security reasons:
// if you uncomment this, then change the real decrypt method to _decrypt.
/*
public Object decrypt(String keyName, String encryptedString) throws EntityCryptoException {
Object result = _decrypt(keyName, encryptedString);
Debug.logInfo("Decrypted value [%s] to result: %s", module, encryptedString, decryptedObj);
return result;
}
*/
/** Decrypts a hex encoded String into an Object */
public Object decrypt(String keyName, EncryptMethod encryptMethod, String encryptedString) throws EntityCryptoException {
try {
return doDecrypt(keyName, encryptMethod, encryptedString, handlers[0]);
} catch (GeneralException e) {
Debug.logInfo("Decrypt with DES key from standard key name hash failed, trying old/funny variety of key name hash", module);
for (int i = 1; i < handlers.length; i++) {
try {
// try using the old/bad hex encoding approach; this is another path the code may take, ie if there is an exception thrown in decrypt
return doDecrypt(keyName, encryptMethod, encryptedString, handlers[i]);
} catch (GeneralException e1) {
// NOTE: this throws the original exception back, not the new one if it fails using the other approach
//throw new EntityCryptoException(e);
}
}
throw new EntityCryptoException(e);
}
}
protected Object doDecrypt(String keyName, EncryptMethod encryptMethod, String encryptedString, StorageHandler handler) throws GeneralException {
byte[] key = this.findKey(keyName, handler);
if (key == null) {
throw new EntityCryptoException("key(" + keyName + ") not found in database");
}
byte[] decryptedBytes = handler.decryptValue(key, encryptMethod, encryptedString);
try {
return UtilObject.getObjectException(decryptedBytes);
} catch (ClassNotFoundException e) {
throw new GeneralException(e);
} catch (IOException e) {
throw new GeneralException(e);
}
}
protected byte[] findKey(String originalKeyName, StorageHandler handler) throws EntityCryptoException {
String hashedKeyName = handler.getHashedKeyName(originalKeyName);
String keyMapName = handler.getKeyMapPrefix(hashedKeyName) + hashedKeyName;
if (keyMap.containsKey(keyMapName)) {
return keyMap.get(keyMapName);
}
// it's ok to run the bulk of this method unlocked or
// unprotected; since the same result will occur even if
// multiple threads request the same key, there is no
// need to protected this block of code.
GenericValue keyValue = null;
try {
keyValue = EntityQuery.use(delegator).from("EntityKeyStore").where("keyName", hashedKeyName).queryOne();
} catch (GenericEntityException e) {
throw new EntityCryptoException(e);
}
if (keyValue == null || keyValue.get("keyText") == null) {
return null;
}
try {
byte[] keyBytes = handler.decodeKeyBytes(keyValue.getString("keyText"));
keyMap.putIfAbsent(keyMapName, keyBytes);
// Do not remove the next line, it's there to handle the
// case of multiple threads trying to find the same key
// both threads will do the findOne call, only one will
// succeed at the putIfAbsent, but both will then fetch
// the same value with the following get().
return keyMap.get(keyMapName);
} catch (GeneralException e) {
throw new EntityCryptoException(e);
}
}
protected void createKey(String originalKeyName, StorageHandler handler, EncryptMethod encryptMethod) throws EntityCryptoException {
String hashedKeyName = handler.getHashedKeyName(originalKeyName);
Key key = handler.generateNewKey();
final GenericValue newValue = delegator.makeValue("EntityKeyStore");
try {
newValue.set("keyText", handler.encodeKey(key.getEncoded()));
} catch (GeneralException e) {
throw new EntityCryptoException(e);
}
newValue.set("keyName", hashedKeyName);
try {
TransactionUtil.doNewTransaction(new Callable<Void>() {
public Void call() throws Exception {
delegator.create(newValue);
return null;
}
}, "storing encrypted key", 0, true);
} catch (GenericEntityException e) {
throw new EntityCryptoException(e);
}
}
protected abstract static class StorageHandler {
protected abstract Key generateNewKey() throws EntityCryptoException;
protected abstract String getHashedKeyName(String originalKeyName);
protected abstract String getKeyMapPrefix(String hashedKeyName);
protected abstract byte[] decodeKeyBytes(String keyText) throws GeneralException;
protected abstract String encodeKey(byte[] key) throws GeneralException;
protected abstract byte[] decryptValue(byte[] key, EncryptMethod encryptMethod, String encryptedString) throws GeneralException;
protected abstract String encryptValue(EncryptMethod encryptMethod, byte[] key, byte[] objBytes) throws GeneralException;
}
protected static final class ShiroStorageHandler extends StorageHandler {
private final HashService hashService;
private final AesCipherService cipherService;
private final AesCipherService saltedCipherService;
private final byte[] kek;
protected ShiroStorageHandler(byte[] kek) {
hashService = new DefaultHashService();
cipherService = new AesCipherService();
cipherService.setMode(OperationMode.ECB);
saltedCipherService = new AesCipherService();
this.kek = kek;
}
@Override
protected Key generateNewKey() {
return saltedCipherService.generateNewKey();
}
@Override
protected String getHashedKeyName(String originalKeyName) {
HashRequest hashRequest = new HashRequest.Builder().setSource(originalKeyName).build();
return hashService.computeHash(hashRequest).toBase64();
}
@Override
protected String getKeyMapPrefix(String hashedKeyName) {
return "{shiro}";
}
@Override
protected byte[] decodeKeyBytes(String keyText) throws GeneralException {
byte[] keyBytes = Base64.decodeBase64(keyText);
if (kek != null) {
keyBytes = saltedCipherService.decrypt(keyBytes, kek).getBytes();
}
return keyBytes;
}
@Override
protected String encodeKey(byte[] key) throws GeneralException {
if (kek != null) {
return saltedCipherService.encrypt(key, kek).toBase64();
} else {
return Base64.encodeBase64String(key);
}
}
@Override
protected byte[] decryptValue(byte[] key, EncryptMethod encryptMethod, String encryptedString) throws GeneralException {
switch (encryptMethod) {
case SALT:
return saltedCipherService.decrypt(Base64.decodeBase64(encryptedString), key).getBytes();
default:
return cipherService.decrypt(Base64.decodeBase64(encryptedString), key).getBytes();
}
}
@Override
protected String encryptValue(EncryptMethod encryptMethod, byte[] key, byte[] objBytes) throws GeneralException {
switch (encryptMethod) {
case SALT:
return saltedCipherService.encrypt(objBytes, key).toBase64();
default:
return cipherService.encrypt(objBytes, key).toBase64();
}
}
}
protected static abstract class LegacyStorageHandler extends StorageHandler {
@Override
protected Key generateNewKey() throws EntityCryptoException {
try {
return DesCrypt.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new EntityCryptoException(e);
}
}
@Override
protected byte[] decodeKeyBytes(String keyText) throws GeneralException {
return StringUtil.fromHexString(keyText);
}
@Override
protected String encodeKey(byte[] key) {
return StringUtil.toHexString(key);
}
@Override
protected byte[] decryptValue(byte[] key, EncryptMethod encryptMethod, String encryptedString) throws GeneralException {
return DesCrypt.decrypt(DesCrypt.getDesKey(key), StringUtil.fromHexString(encryptedString));
}
@Override
protected String encryptValue(EncryptMethod encryptMethod, byte[] key, byte[] objBytes) throws GeneralException {
return StringUtil.toHexString(DesCrypt.encrypt(DesCrypt.getDesKey(key), objBytes));
}
};
protected static final StorageHandler OldFunnyHashStorageHandler = new LegacyStorageHandler() {
@Override
protected String getHashedKeyName(String originalKeyName) {
return HashCrypt.digestHashOldFunnyHex(null, originalKeyName);
}
@Override
protected String getKeyMapPrefix(String hashedKeyName) {
return "{funny-hash}";
}
};
protected static final StorageHandler NormalHashStorageHandler = new LegacyStorageHandler() {
@Override
protected String getHashedKeyName(String originalKeyName) {
return HashCrypt.digestHash("SHA", originalKeyName.getBytes());
}
@Override
protected String getKeyMapPrefix(String hashedKeyName) {
return "{normal-hash}";
}
};
protected static final class SaltedBase64StorageHandler extends StorageHandler {
private final Key kek;
protected SaltedBase64StorageHandler(byte[] kek) throws EntityCryptoException {
Key key = null;
if (kek != null) {
try {
key = DesCrypt.getDesKey(kek);
} catch (GeneralException e) {
Debug.logInfo("Invalid key-encryption-key specified for SaltedBase64StorageHandler; the key is probably valid for the newer ShiroStorageHandler", module);
}
}
this.kek = key;
}
@Override
protected Key generateNewKey() throws EntityCryptoException {
try {
return DesCrypt.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new EntityCryptoException(e);
}
}
@Override
protected String getHashedKeyName(String originalKeyName) {
return HashCrypt.digestHash64("SHA", originalKeyName.getBytes());
}
@Override
protected String getKeyMapPrefix(String hashedKeyName) {
return "{salted-base64}";
}
@Override
protected byte[] decodeKeyBytes(String keyText) throws GeneralException {
byte[] keyBytes = Base64.decodeBase64(keyText);
if (kek != null) {
keyBytes = DesCrypt.decrypt(kek, keyBytes);
}
return keyBytes;
}
@Override
protected String encodeKey(byte[] key) throws GeneralException {
if (kek != null) {
key = DesCrypt.encrypt(kek, key);
}
return Base64.encodeBase64String(key);
}
@Override
protected byte[] decryptValue(byte[] key, EncryptMethod encryptMethod, String encryptedString) throws GeneralException {
byte[] allBytes = DesCrypt.decrypt(DesCrypt.getDesKey(key), Base64.decodeBase64(encryptedString));
int length = allBytes[0];
byte[] objBytes = new byte[allBytes.length - 1 - length];
System.arraycopy(allBytes, 1 + length, objBytes, 0, objBytes.length);
return objBytes;
}
@Override
protected String encryptValue(EncryptMethod encryptMethod, byte[] key, byte[] objBytes) throws GeneralException {
byte[] saltBytes;
switch (encryptMethod) {
case SALT:
Random random = new SecureRandom();
// random length 5-16
saltBytes = new byte[5 + random.nextInt(11)];
random.nextBytes(saltBytes);
break;
default:
saltBytes = new byte[0];
break;
}
byte[] allBytes = new byte[1 + saltBytes.length + objBytes.length];
allBytes[0] = (byte) saltBytes.length;
System.arraycopy(saltBytes, 0, allBytes, 1, saltBytes.length);
System.arraycopy(objBytes, 0, allBytes, 1 + saltBytes.length, objBytes.length);
String result = Base64.encodeBase64String(DesCrypt.encrypt(DesCrypt.getDesKey(key), allBytes));
return result;
}
};
}