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}