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}