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