001// -------------------------------------------------------------------------------- 002// Copyright 2002-2024 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.util.server.persistence; 018 019import com.echothree.model.control.core.server.control.CoreControl; 020import static com.echothree.model.control.core.common.workflow.BaseEncryptionKeyStatusConstants.WorkflowStep_BASE_ENCRYPTION_KEY_STATUS_ACTIVE; 021import static com.echothree.model.control.core.common.workflow.BaseEncryptionKeyStatusConstants.Workflow_BASE_ENCRYPTION_KEY_STATUS; 022import com.echothree.model.control.workflow.server.control.WorkflowControl; 023import com.echothree.model.data.core.server.entity.BaseEncryptionKey; 024import com.echothree.model.data.core.server.entity.EntityEncryptionKey; 025import com.echothree.model.data.core.server.entity.EntityInstance; 026import com.echothree.model.data.party.common.pk.PartyPK; 027import com.echothree.model.data.workflow.server.entity.WorkflowEntityStatus; 028import com.echothree.util.common.exception.PersistenceEncryptionException; 029import com.echothree.util.common.persistence.EncryptionConstants; 030import com.echothree.util.common.string.MD5Utils; 031import com.echothree.util.common.persistence.BaseKey; 032import com.echothree.util.common.persistence.BaseKeys; 033import com.echothree.util.server.message.ExecutionErrorAccumulator; 034import com.google.common.base.Charsets; 035import com.google.common.io.BaseEncoding; 036import java.security.InvalidAlgorithmParameterException; 037import java.security.InvalidKeyException; 038import java.security.NoSuchAlgorithmException; 039import java.security.SecureRandom; 040import java.util.List; 041import java.util.Random; 042import javax.crypto.BadPaddingException; 043import javax.crypto.Cipher; 044import javax.crypto.IllegalBlockSizeException; 045import javax.crypto.KeyGenerator; 046import javax.crypto.NoSuchPaddingException; 047import javax.crypto.SecretKey; 048import javax.crypto.spec.IvParameterSpec; 049import javax.crypto.spec.SecretKeySpec; 050import org.apache.commons.logging.Log; 051import org.apache.commons.logging.LogFactory; 052import org.infinispan.Cache; 053 054public class EncryptionUtils { 055 056 private static final String externalPrefix = "#EXTERNAL#"; 057 private static final String fqnBaseKeys = "/com/echothree/security/base"; 058 private static final String cacheBaseKey1 = "BaseKey1"; 059 private static final String cacheBaseKey2 = "BaseKey2"; 060 061 protected Log log = LogFactory.getLog(EncryptionUtils.class); 062 063 private EncryptionUtils() { 064 super(); 065 } 066 067 private static class EncryptionUtilsHolder { 068 static EncryptionUtils instance = new EncryptionUtils(); 069 } 070 071 public static EncryptionUtils getInstance() { 072 return EncryptionUtilsHolder.instance; 073 } 074 075 public Random getRandom() { 076 Random random; 077 078 try { 079 random = SecureRandom.getInstance(EncryptionConstants.randomAlgorithm); 080 } catch (NoSuchAlgorithmException nsae) { 081 log.warn("SecureRandom was unable to find an instance of " + EncryptionConstants.randomAlgorithm + ", falling back to Random"); 082 random = new Random(); 083 } 084 085 return random; 086 } 087 088 private byte[] generateInitializationVector() { 089 byte[] bytes = new byte[16]; 090 091 getRandom().nextBytes(bytes); 092 093 return bytes; 094 } 095 096 private BaseKey generateBaseKey(final String cacheBaseKeyName) { 097 SecretKey secretKey; 098 099 try { 100 KeyGenerator keyGenerator = KeyGenerator.getInstance(EncryptionConstants.algorithm); 101 102 keyGenerator.init(EncryptionConstants.keysize); 103 104 secretKey = keyGenerator.generateKey(); 105 } catch (NoSuchAlgorithmException nsae) { 106 throw new PersistenceEncryptionException(nsae); 107 } 108 109 Cache<String, Object> cache = ThreadCaches.currentCaches().getSecurityCache(); 110 byte[] iv = generateInitializationVector(); 111 112 BaseKey baseKey = new BaseKey(secretKey, iv); 113 cache.put(fqnBaseKeys + "/" + cacheBaseKeyName, baseKey); 114 115 return baseKey; 116 } 117 118 // From: http://forum.java.sun.com/thread.jspa?threadID=471971&messageID=2182261 119 private byte[] xorArrays(byte[] a, byte[] b) throws IllegalArgumentException { 120 if(b.length != a.length) { 121 throw new IllegalArgumentException("length of byte[] b must be == length of byte[] a"); 122 } 123 124 byte[] c = new byte[a.length]; 125 for(int i = 0; i < a.length; i++) { 126 c[i] = (byte)(a[i] ^ b[i]); 127 } 128 129 return c; 130 } 131 132 private BaseKey xorBaseKeys(BaseKey baseKey1, BaseKey baseKey2) { 133 SecretKey key3 = new SecretKeySpec(xorArrays(baseKey1.getKey().getEncoded(), baseKey2.getKey().getEncoded()), "AES"); 134 byte[] iv3 = xorArrays(baseKey1.getIv(), baseKey2.getIv()); 135 136 return new BaseKey(key3, iv3); 137 } 138 139 private BaseKeys createBaseKeys(final ExecutionErrorAccumulator eea, final PartyPK createdBy) { 140 var coreControl = Session.getModelController(CoreControl.class); 141 BaseKey baseKey1 = generateBaseKey(cacheBaseKey1); 142 BaseKey baseKey2 = generateBaseKey(cacheBaseKey2); 143 BaseKey baseKey3 = xorBaseKeys(baseKey1, baseKey2); 144 BaseEncryptionKey baseEncryptionKey = coreControl.createBaseEncryptionKey(eea, baseKey1, baseKey2, createdBy); 145 146 return baseEncryptionKey == null? null: new BaseKeys(baseKey1, baseKey2, baseKey3, baseEncryptionKey.getBaseEncryptionKeyName()); 147 } 148 149 public BaseKeys generateBaseKeys(final ExecutionErrorAccumulator eea, final PartyPK createdBy) { 150 var coreControl = Session.getModelController(CoreControl.class); 151 BaseKeys baseKeys = null; 152 153 if(coreControl.countEntityEncryptionKeys() == 0) { 154 baseKeys = createBaseKeys(eea, createdBy); 155 log.info(baseKeys == null? "Base Encryption Keys Not Generated": "Base Encryption Keys Generated"); 156 } else { 157 log.error("Base Encryption Keys Already Exist"); 158 } 159 160 return baseKeys; 161 } 162 163 private void validateBaseKeys(final BaseKeys baseKeys, final int minimumKeysRequired) { 164 int baseKeyCount = baseKeys.getBaseKeyCount(); 165 166 if(baseKeyCount < minimumKeysRequired) { 167 throw new PersistenceEncryptionException(baseKeyCount == 0 ? "Base Encryption Keys Missing" : "Base Encryption Keys Incomplete"); 168 } else { 169 BaseKey baseKey1 = baseKeys.getBaseKey1(); 170 BaseKey baseKey2 = baseKeys.getBaseKey2(); 171 BaseKey baseKey3 = baseKeys.getBaseKey3(); 172 173 if(baseKeyCount == 2 && (baseKey1 == null || baseKey2 == null)) { 174 // Recovery using third key is needed. 175 if(baseKey1 == null) { 176 // Recover baseKey1 177 baseKey1 = xorBaseKeys(baseKey2, baseKey3); 178 baseKeys.setBaseKey1(baseKey1); 179 } else { 180 // Recover baseKey2 181 baseKey2 = xorBaseKeys(baseKey1, baseKey3); 182 baseKeys.setBaseKey2(baseKey2); 183 } 184 } else if(baseKeyCount == 3) { 185 // Verify third key is correct based on first two. 186 if(!baseKey3.equals(xorBaseKeys(baseKey1, baseKey2))) { 187 throw new PersistenceEncryptionException("Third key is not correct"); 188 } 189 } 190 191 var coreControl = Session.getModelController(CoreControl.class); 192 String sha1Hash = Sha1Utils.getInstance().encode(baseKey1, baseKey2); 193 BaseEncryptionKey baseEncryptionKey = coreControl.getBaseEncryptionKeyBySha1Hash(sha1Hash); 194 195 if(baseEncryptionKey != null) { 196 var workflowControl = Session.getModelController(WorkflowControl.class); 197 EntityInstance entityInstance = coreControl.getEntityInstanceByBasePK(baseEncryptionKey.getPrimaryKey()); 198 WorkflowEntityStatus workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceUsingNames(Workflow_BASE_ENCRYPTION_KEY_STATUS, entityInstance); 199 200 if(!workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName().equals(WorkflowStep_BASE_ENCRYPTION_KEY_STATUS_ACTIVE)) { 201 throw new PersistenceEncryptionException("Supplied Base Encryption Keys Not Active"); 202 } 203 } else { 204 throw new PersistenceEncryptionException("Supplied Base Encryption Keys Not Valid"); 205 } 206 } 207 } 208 209 public void loadBaseKeys(final BaseKeys baseKeys) { 210 Cache<String, Object> cache = ThreadCaches.currentCaches().getSecurityCache(); 211 212 validateBaseKeys(baseKeys, 2); 213 214 cache.put(fqnBaseKeys + "/" + cacheBaseKey1, baseKeys.getBaseKey1()); 215 cache.put(fqnBaseKeys + "/" + cacheBaseKey2, baseKeys.getBaseKey2()); 216 log.info("Base Encryption Keys Loaded"); 217 } 218 219 public BaseKeys changeBaseKeys(final ExecutionErrorAccumulator eea, final BaseKeys oldBaseKeys, 220 final PartyPK changedBy) { 221 var coreControl = Session.getModelController(CoreControl.class); 222 223 validateBaseKeys(oldBaseKeys, 3); 224 BaseKeys newBaseKeys = createBaseKeys(eea, changedBy); 225 226 Cipher oldCipher1 = getInitializedCipher(oldBaseKeys.getKey1(), oldBaseKeys.getIv1(), Cipher.DECRYPT_MODE); 227 Cipher oldCipher2 = getInitializedCipher(oldBaseKeys.getKey2(), oldBaseKeys.getIv2(), Cipher.DECRYPT_MODE); 228 Cipher newCipher1 = getInitializedCipher(newBaseKeys.getKey1(), newBaseKeys.getIv1(), Cipher.ENCRYPT_MODE); 229 Cipher newCipher2 = getInitializedCipher(newBaseKeys.getKey2(), newBaseKeys.getIv2(), Cipher.ENCRYPT_MODE); 230 231 List<EntityEncryptionKey> entityEncryptionKeys = coreControl.getEntityEncryptionKeysForUpdate(); 232 233 for(var entityEncryptionKey : entityEncryptionKeys) { 234 BaseEncoding baseEncoding = BaseEncoding.base64(); 235 byte[] encryptedKey = baseEncoding.decode(entityEncryptionKey.getSecretKey()); 236 byte[] encryptedIv = baseEncoding.decode(entityEncryptionKey.getInitializationVector()); 237 238 try { 239 byte[] decryptedKey = oldCipher1.doFinal(oldCipher2.doFinal(encryptedKey)); 240 byte[] decryptedIv = oldCipher1.doFinal(oldCipher2.doFinal(encryptedIv)); 241 242 encryptedKey = newCipher2.doFinal(newCipher1.doFinal(decryptedKey)); 243 encryptedIv = newCipher2.doFinal(newCipher1.doFinal(decryptedIv)); 244 } catch (IllegalStateException ise) { 245 throw new PersistenceEncryptionException(ise); 246 } catch (IllegalBlockSizeException ibse) { 247 throw new PersistenceEncryptionException(ibse); 248 } catch (BadPaddingException bpe) { 249 throw new PersistenceEncryptionException(bpe); 250 } 251 252 entityEncryptionKey.setSecretKey(baseEncoding.encode(encryptedKey)); 253 entityEncryptionKey.setInitializationVector(baseEncoding.encode(encryptedIv)); 254 } 255 256 log.info("Base Encryption Keys Changed"); 257 258 return newBaseKeys; 259 } 260 261 private BaseKeys getBaseKeys() { 262 Cache<String, Object> cache = ThreadCaches.currentCaches().getSecurityCache(); 263 BaseKey baseKey1 = (BaseKey)cache.get(fqnBaseKeys + "/" + cacheBaseKey1); 264 BaseKey baseKey2 = (BaseKey)cache.get(fqnBaseKeys + "/" + cacheBaseKey2); 265 266 int cacheCount = (baseKey1 == null ? 0 : 1) + (baseKey2 == null ? 0 : 1); 267 268 if(cacheCount != 2) { 269 throw new PersistenceEncryptionException(cacheCount == 0 ? "Base Encryption Keys Missing" : "Base Encryption Keys Incomplete"); 270 } 271 272 BaseKeys baseKeys = new BaseKeys(baseKey1, baseKey2); 273 274 return baseKeys; 275 } 276 277 public String encrypt(final String entityTypeName, final String entityColumnName, final String value) { 278 return encrypt(entityTypeName, entityColumnName, Boolean.FALSE, value); 279 } 280 281 public String encrypt(final String entityTypeName, final String entityColumnName, final Boolean isExternal, final String value) { 282 String encryptedValue = null; 283 284 if(value != null) { 285 try { 286 encryptedValue = BaseEncoding.base64().encode(getCipher(entityTypeName, entityColumnName, isExternal, Cipher.ENCRYPT_MODE).doFinal(value.getBytes(Charsets.UTF_8))); 287 } catch (IllegalStateException ise) { 288 throw new PersistenceEncryptionException(ise); 289 } catch (IllegalBlockSizeException ibse) { 290 throw new PersistenceEncryptionException(ibse); 291 } catch (BadPaddingException bpe) { 292 throw new PersistenceEncryptionException(bpe); 293 } 294 } 295 296 return encryptedValue; 297 } 298 299 public String decrypt(final String entityTypeName, final String entityColumnName, final String value) { 300 return decrypt(entityTypeName, entityColumnName, Boolean.FALSE, value); 301 } 302 303 public String decrypt(final String entityTypeName, final String entityColumnName, Boolean isExternal, final String value) { 304 String decryptedValue = null; 305 306 if(value != null) { 307 try { 308 decryptedValue = new String(getCipher(entityTypeName, entityColumnName, isExternal, Cipher.DECRYPT_MODE).doFinal(BaseEncoding.base64().decode(value)), Charsets.UTF_8); 309 } catch (IllegalStateException ise) { 310 throw new PersistenceEncryptionException(ise); 311 } catch (IllegalBlockSizeException ibse) { 312 throw new PersistenceEncryptionException(ibse); 313 } catch (BadPaddingException bpe) { 314 throw new PersistenceEncryptionException(bpe); 315 } 316 } 317 318 return decryptedValue; 319 } 320 321 private Cipher getInitializedCipher(final SecretKey secretKey, final byte[] iv, final int cipherMode) { 322 Cipher cipher = null; 323 324 try { 325 cipher = Cipher.getInstance(EncryptionConstants.transformation); 326 } catch (NoSuchAlgorithmException nsae) { 327 throw new PersistenceEncryptionException(nsae); 328 } catch (NoSuchPaddingException nspe) { 329 throw new PersistenceEncryptionException(nspe); 330 } 331 332 // Setup cipher 333 try { 334 IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); 335 336 cipher.init(cipherMode, secretKey, ivParameterSpec); 337 } catch (InvalidKeyException ike) { 338 throw new PersistenceEncryptionException(ike); 339 } catch (InvalidAlgorithmParameterException iape) { 340 throw new PersistenceEncryptionException(iape); 341 } 342 343 return cipher; 344 } 345 346 private byte[] encryptDataUsingBaseKeys(final BaseKeys baseKeys, final byte[] decryptedData) { 347 Cipher cipher1 = getInitializedCipher(baseKeys.getKey1(), baseKeys.getIv1(), Cipher.ENCRYPT_MODE); 348 Cipher cipher2 = getInitializedCipher(baseKeys.getKey2(), baseKeys.getIv2(), Cipher.ENCRYPT_MODE); 349 byte[] encryptedData = null; 350 351 try { 352 encryptedData = cipher2.doFinal(cipher1.doFinal(decryptedData)); 353 } catch (IllegalStateException ise) { 354 throw new PersistenceEncryptionException(ise); 355 } catch (IllegalBlockSizeException ibse) { 356 throw new PersistenceEncryptionException(ibse); 357 } catch (BadPaddingException bpe) { 358 throw new PersistenceEncryptionException(bpe); 359 } 360 361 return encryptedData; 362 } 363 364 private byte[] decryptDataUsingBaseKeys(final BaseKeys baseKeys, final byte[] encryptedData) { 365 Cipher cipher1 = getInitializedCipher(baseKeys.getKey1(), baseKeys.getIv1(), Cipher.DECRYPT_MODE); 366 Cipher cipher2 = getInitializedCipher(baseKeys.getKey2(), baseKeys.getIv2(), Cipher.DECRYPT_MODE); 367 byte[] decryptedData = null; 368 369 try { 370 decryptedData = cipher1.doFinal(cipher2.doFinal(encryptedData)); 371 } catch (IllegalStateException ise) { 372 throw new PersistenceEncryptionException(ise); 373 } catch (IllegalBlockSizeException ibse) { 374 throw new PersistenceEncryptionException(ibse); 375 } catch (BadPaddingException bpe) { 376 throw new PersistenceEncryptionException(bpe); 377 } 378 379 return decryptedData; 380 } 381 382 private Cipher getCipher(final String entityTypeName, final String entityColumnName, final Boolean isExternal, final int cipherMode) { 383 var coreControl = Session.getModelController(CoreControl.class); 384 String entityEncryptionKeyName = MD5Utils.getInstance().encode(new StringBuilder(entityTypeName).append('.').append(isExternal? externalPrefix: entityColumnName).toString()); 385 SecretKey secretKey; 386 byte[] iv; 387 388 BaseEncoding baseEncoding = BaseEncoding.base64(); 389 EntityEncryptionKey entityEncryptionKey = coreControl.getEntityEncryptionKeyByName(entityEncryptionKeyName); 390 BaseKeys baseKeys = getBaseKeys(); 391 392 if(entityEncryptionKey == null) { 393 // Key has not yet been generated for this EntityType 394 try { 395 KeyGenerator keyGenerator = KeyGenerator.getInstance(EncryptionConstants.algorithm); 396 397 keyGenerator.init(EncryptionConstants.keysize); 398 399 secretKey = keyGenerator.generateKey(); 400 } catch (NoSuchAlgorithmException nsae) { 401 throw new PersistenceEncryptionException(nsae); 402 } 403 404 byte[] key = secretKey.getEncoded(); 405 iv = generateInitializationVector(); 406 407 byte[] encryptedKey = encryptDataUsingBaseKeys(baseKeys, key); 408 byte[] encryptedIv = encryptDataUsingBaseKeys(baseKeys, iv); 409 410 coreControl.createEntityEncryptionKey(entityEncryptionKeyName, isExternal, 411 baseEncoding.encode(encryptedKey), baseEncoding.encode(encryptedIv)); 412 } else { 413 // Key has been generated for this EntityType 414 byte[] encryptedKey = baseEncoding.decode(entityEncryptionKey.getSecretKey()); 415 byte[] encryptedIv = baseEncoding.decode(entityEncryptionKey.getInitializationVector()); 416 417 byte[] key = decryptDataUsingBaseKeys(baseKeys, encryptedKey); 418 iv = decryptDataUsingBaseKeys(baseKeys, encryptedIv); 419 420 secretKey = new SecretKeySpec(key, EncryptionConstants.algorithm); 421 } 422 423 return getInitializedCipher(secretKey, iv, cipherMode); 424 } 425 426}