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}