001/**
002 * Copyright 2005-2015 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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/*
017 * To change this template, choose Tools | Templates
018 * and open the template in the editor.
019 */
020package org.kuali.rice.krad.devtools.maintainablexml;
021
022import java.io.UnsupportedEncodingException;
023import java.security.GeneralSecurityException;
024import java.security.MessageDigest;
025
026import javax.crypto.Cipher;
027import javax.crypto.KeyGenerator;
028import javax.crypto.SecretKey;
029import javax.crypto.SecretKeyFactory;
030import javax.crypto.spec.DESKeySpec;
031
032import org.apache.commons.codec.binary.Base64;
033import org.apache.commons.lang.StringUtils;
034
035/**
036 * Implementation of encryption service for demonstration.
037 * This class has been copied from the base rice code but has added an old secret key that allows for data encrypted
038 * with the commons-codec 1.3 api to be decrypted with newer versions of the api. The Base64.decodeBase64 method did not
039 * decode the last two bytes correctly in 1.3 when the encoded key did not end with '=', it always defaulted those
040 * bytes to '1'
041 * 
042 * @author Kuali Rice Team (rice.collab@kuali.org)
043 */
044@SuppressWarnings({"InsecureCipherMode", "InsecureCryptoUsage"})
045public class EncryptionService {
046
047    public final static String ALGORITHM = "DES/ECB/PKCS5Padding";
048    public final static String HASH_ALGORITHM = "SHA";
049    private final static String CHARSET = "UTF-8";
050    private transient SecretKey desKey;
051    private transient SecretKey desKeyOld;
052    private boolean isEnabled = false;
053
054    public EncryptionService(String key) throws Exception {
055        if (desKey != null) {
056            throw new RuntimeException("The secret key must be kept secret. Storing it in the Java source code is a really bad idea.");
057        }
058
059        if (!StringUtils.isEmpty(key)) {
060            setSecretKey(key);
061        }
062    }
063
064    public boolean isEnabled() {
065        return isEnabled;
066    }
067
068    public String encrypt(Object valueToHide) throws GeneralSecurityException {
069        checkEnabled();
070
071        if (valueToHide == null) {
072            return "";
073        }
074
075        // Initialize the cipher for encryption
076        Cipher cipher = Cipher.getInstance(ALGORITHM);
077        cipher.init(Cipher.ENCRYPT_MODE, getDesKey());
078
079        try {
080            // Our cleartext
081            byte[] cleartext = valueToHide.toString().getBytes(CHARSET);
082
083            // Encrypt the cleartext
084            byte[] ciphertext = cipher.doFinal(cleartext);
085
086            return new String(Base64.encodeBase64(ciphertext), CHARSET);
087        } catch (Exception e) {
088            throw new RuntimeException(e);
089        }
090
091    }
092
093    public String decrypt(String ciphertext) throws GeneralSecurityException {
094        checkEnabled();
095
096        if (StringUtils.isBlank(ciphertext)) {
097            return "";
098        }
099
100        // Initialize the same cipher for decryption
101        Cipher cipher = Cipher.getInstance(ALGORITHM);
102        cipher.init(Cipher.DECRYPT_MODE, getDesKey());
103
104        try {
105            // un-Base64 encode the encrypted data
106            byte[] encryptedData = Base64.decodeBase64(ciphertext.getBytes(CHARSET));
107
108            // Decrypt the ciphertext
109            byte[] cleartext1 = cipher.doFinal(encryptedData);
110            return new String(cleartext1, CHARSET);
111        } catch (Exception e) {
112            Cipher cipher2 = Cipher.getInstance(ALGORITHM);
113            cipher2.init(Cipher.DECRYPT_MODE, getDesKeyOld());
114
115            try {
116                // un-Base64 encode the encrypted data
117                byte[] encryptedData = Base64.decodeBase64(ciphertext.getBytes(CHARSET));
118
119                // Decrypt the ciphertext
120                byte[] cleartext1 = cipher2.doFinal(encryptedData);
121                return new String(cleartext1, CHARSET);
122            } catch (UnsupportedEncodingException ex) {
123                throw new RuntimeException(e);
124            }            
125        }
126    }
127
128    public byte[] encryptBytes(byte[] valueToHide) throws GeneralSecurityException {
129        checkEnabled();
130
131        if (valueToHide == null) {
132            return new byte[0];
133        }
134
135        // Initialize the cipher for encryption
136        Cipher cipher = Cipher.getInstance(ALGORITHM);
137        cipher.init(Cipher.ENCRYPT_MODE, getDesKey());
138
139        // Our cleartext
140        byte[] cleartext = valueToHide;
141
142        // Encrypt the cleartext
143        byte[] ciphertext = cipher.doFinal(cleartext);
144
145        return ciphertext;
146    }
147
148    public byte[] decryptBytes(byte[] ciphertext) throws GeneralSecurityException {
149        checkEnabled();
150
151        if (ciphertext == null) {
152            return new byte[0];
153        }
154
155        // Initialize the same cipher for decryption
156        Cipher cipher = Cipher.getInstance(ALGORITHM);
157        cipher.init(Cipher.DECRYPT_MODE, getDesKey());
158
159        // un-Base64 encode the encrypted data
160        byte[] encryptedData = ciphertext;
161
162        // Decrypt the ciphertext
163        byte[] cleartext1 = cipher.doFinal(encryptedData);
164        return cleartext1;
165    }
166
167    /**
168     * 
169     * This method generates keys. This method is implementation specific and should not be present in any general purpose interface
170     * extracted from this class.
171     * 
172     * @return
173     * @throws Exception
174     */
175    public static String generateEncodedKey() throws Exception {
176        KeyGenerator keygen = KeyGenerator.getInstance("DES");
177        SecretKey desKey = keygen.generateKey();
178
179        // Create the cipher
180        Cipher cipher = Cipher.getInstance(ALGORITHM);
181        cipher.init((Cipher.WRAP_MODE), desKey);
182
183        SecretKeyFactory desFactory = SecretKeyFactory.getInstance("DES");
184        DESKeySpec desSpec = (DESKeySpec) desFactory.getKeySpec(desKey, javax.crypto.spec.DESKeySpec.class);
185        byte[] rawDesKey = desSpec.getKey();
186
187        return new String(Base64.encodeBase64(rawDesKey));
188    }
189
190    private SecretKey unwrapEncodedKey(String key) throws Exception {
191        KeyGenerator keygen = KeyGenerator.getInstance("DES");
192        SecretKey desKey = keygen.generateKey();
193
194        // Create the cipher
195        Cipher cipher = Cipher.getInstance(ALGORITHM);
196        cipher.init((Cipher.UNWRAP_MODE), desKey);
197
198        byte[] bytes = Base64.decodeBase64(key.getBytes());
199
200        SecretKeyFactory desFactory = SecretKeyFactory.getInstance("DES");
201
202        DESKeySpec keyspec = new DESKeySpec(bytes);
203        SecretKey k = desFactory.generateSecret(keyspec);
204
205        return k;
206
207    }
208
209    private SecretKey unwrapEncodedKeyOld(String key) throws Exception {
210        KeyGenerator keygen = KeyGenerator.getInstance("DES");
211        SecretKey desKey = keygen.generateKey();
212
213        // Create the cipher
214        Cipher cipher = Cipher.getInstance(ALGORITHM);
215        cipher.init((Cipher.UNWRAP_MODE), desKey);
216
217        byte[] bytes = Base64.decodeBase64(key.getBytes());
218
219
220        // If decoding was done with commons-codec 1.3 and the key not ended with '='
221        bytes[6] = 1;
222        bytes[7] = 1;
223
224        SecretKeyFactory desFactory = SecretKeyFactory.getInstance("DES");
225
226        DESKeySpec keyspec = new DESKeySpec(bytes);
227        SecretKey k = desFactory.generateSecret(keyspec);
228
229        return k;
230
231    }
232
233    /**
234     * Sets the secretKey attribute value.
235     * 
236     * @param secretKey The secretKey to set.
237     * @throws Exception
238     */
239    public void setSecretKey(String secretKey) throws Exception {
240        if (!StringUtils.isEmpty(secretKey)) {
241            desKey = this.unwrapEncodedKey(secretKey);
242            setDesKeyOld(this.unwrapEncodedKeyOld(secretKey));
243            isEnabled = true;
244            // Create the cipher
245            Cipher cipher = Cipher.getInstance(ALGORITHM);
246            cipher.init((Cipher.WRAP_MODE), getDesKey());
247        }
248    }
249
250    /** Hash the value by converting to a string, running the hash algorithm, and then base64'ng the results.
251     * Returns a blank string if any problems occur or the input value is null or empty.
252     *
253     */
254    public String hash(Object valueToHide) throws GeneralSecurityException {
255        if (valueToHide == null || StringUtils.isEmpty(valueToHide.toString())) {
256            return "";
257        }
258        try {
259            MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
260            return new String(Base64.encodeBase64(md.digest(valueToHide.toString().getBytes(CHARSET))), CHARSET);
261        } catch (UnsupportedEncodingException ex) {
262            // should never happen
263        }
264        return "";
265    }
266
267    /**
268     * Performs a check to see if the encryption service is enabled.  If it is not then an
269     * IllegalStateException will be thrown.
270     */
271    protected void checkEnabled() {
272        if (!isEnabled()) {
273            throw new IllegalStateException("Illegal use of encryption service.  Encryption service is disabled, to enable please configure 'encryption.key'.");
274        }
275    }
276
277    /**
278     * @return the desKey
279     */
280    public SecretKey getDesKey() {
281        return desKey;
282    }
283
284    /**
285     * @return the desKeyOld
286     */
287    public SecretKey getDesKeyOld() {
288        return desKeyOld;
289    }
290
291    /**
292     * @param desKeyOld the desKeyOld to set
293     */
294    public void setDesKeyOld(SecretKey desKeyOld) {
295        this.desKeyOld = desKeyOld;
296    }
297}