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