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