View Javadoc
1   /**
2    * Copyright 2014 Internet2
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package edu.internet2.middleware.grouperInstaller.morphString;
17  
18  import java.io.InputStream;
19  import java.io.OutputStream;
20  import java.security.NoSuchAlgorithmException;
21  import java.util.Base64;
22  
23  import javax.crypto.Cipher;
24  import javax.crypto.CipherInputStream;
25  import javax.crypto.CipherOutputStream;
26  import javax.crypto.KeyGenerator;
27  import javax.crypto.SecretKey;
28  import javax.crypto.spec.SecretKeySpec;
29  
30  import edu.internet2.middleware.grouperInstaller.util.GrouperInstallerUtils;
31  
32  
33  /**
34   * The purpose of this class is to provide encryption 
35   * and decryption using standard Java libraries, for potentially 
36   * large amounts of data.
37   * <p>
38   * This class provides default encryption using AES with a constant
39   * 128 bit key.  If you want something more secure feel free to 
40   * override the defaults however you please.
41   * <p>
42   * This class works in one of two ways, (1) in memory using Strings, or (2) via 
43   * I/O streams (preferred for large amounts of data).
44   * <p>
45   * Crypo objects, or more specifically the default ciphers they create, are not 
46   * threadsafe and are not computationally cheap, so a threadlocal factory 
47   * method is provided for convenience.  This is the preferred means of usage,
48   * but feel free to create these objects however you please.
49   * <p>
50   * Note that you can encrypt BLOB fields by specifying encryption in the 
51   * configurator (Crypto is the default encryption mechanism for that).
52   * <p> 
53   */
54  public class Crypto {
55   
56    
57    /**
58     * Generate a key.
59     * @param cipherName the name of the cipher, if null will default to "AES"
60     * @param keybits the number of bits in the key, if null will default to 128
61     * @return the bytes comprising the key
62     */
63    public static byte[] generateKeyBytes(String cipherName, Integer keybits) {
64      KeyGenerator keyGenerator = null;
65      cipherName = cipherName == null ? "AES" : cipherName;
66      try {
67        keyGenerator = KeyGenerator.getInstance(cipherName);
68      } catch (NoSuchAlgorithmException e) {
69        throw new RuntimeException("Failed to create KeyGenerator for cipherName="+cipherName, e);
70      }
71      keyGenerator.init(keybits == null ? 128 : keybits); // 192 and 256 bits may not be available
72      
73      // Generate the secret key specs.
74      SecretKey secretKey = keyGenerator.generateKey();
75      byte[] keyBytes = secretKey.getEncoded();
76      
77      return keyBytes;    
78    }
79  
80    
81    /** SecretKeySpec */
82    private SecretKeySpec key;
83    
84    /** lazily constructed cipher */
85    private Cipher cipher = null;
86      
87    /**
88     * Create the default cipher
89     * @return the default cipher
90     */
91    public Cipher createDefaultCipher() {
92      try {
93        return Cipher.getInstance("AES");
94      } catch(Exception x) {
95        throw new RuntimeException("Failed to create the default cipher!", x);
96      }
97    }
98    
99    /** Default crypto object 
100    * @param theKey used to encrypt/decrypt 
101    */
102   public Crypto(String theKey) {
103     super();
104     init(theKey);
105   }
106   
107   /** only warn once */
108   private static boolean warned = false;
109   
110   /** initialize the key and cipher 
111    * @param secret
112    */
113   protected void init(String secret) {
114     
115     if (GrouperInstallerUtils.isBlank(secret)) {
116       throw new NullPointerException("Must supply a non blank encrypt.key");
117     }
118     StringBuilder secretBuilder = new StringBuilder(secret);
119     if (!warned && secret.length() < 8) {
120       //note we dont have a logger
121       System.out.println("morphString warning: secret.key in morphString.properties should be at least 8 chars");
122       warned = true;
123     }
124     //secret must be length 16 or 32.  pad if not
125     while (secretBuilder.length() < 16) {
126       secretBuilder.append("x");
127     }
128     if (secretBuilder.length() > 16) {
129       while (secretBuilder.length() < 32) {
130         secretBuilder.append("x");
131       }
132     }
133     if (secretBuilder.length() > 32) {
134       secretBuilder.delete(32, secretBuilder.length());
135     }
136     secret = secretBuilder.toString();
137     this.key = new SecretKeySpec(secret.getBytes(), "AES");
138     this.cipher = this.createDefaultCipher();
139   }
140   
141   /**
142    * Encrypt the string
143    * @param clearText
144    * @return the encrypted String
145    */
146   public String encrypt(String clearText) {
147     byte[] input = clearText.getBytes();
148     try {
149       this.initCipher(true);
150       byte[] output = this.cipher.doFinal(input);
151       
152       byte[] encoded = Base64.getEncoder().encode(output);   
153       return new String(encoded);
154     } catch(Exception x) {
155       throw new RuntimeException("Failed to encrypt string", x);
156     }
157   } 
158    
159   /**
160    * Decrypt the string
161    * @param cipherText
162    * @return the decrypted string
163    */
164   public String decrypt(String cipherText) {
165     try {      
166         byte[] cipherBytes = cipherText.getBytes();
167         byte[] decodedBytes = Base64.getDecoder().decode(cipherBytes);
168         this.initCipher(false);
169         byte[] clearBytes = this.cipher.doFinal(decodedBytes);
170         return new String(clearBytes);
171     } catch(Exception x) {
172       throw new RuntimeException("Failed to decrypt the cipherText", x);
173     }
174   }
175   
176   /**
177    * Initialize the cipher for encryption or decryption
178    * @param encrypt true to encrypt, false to decrypt
179    */
180   private void initCipher(boolean encrypt) {
181     try {
182       if ( encrypt ) {
183         this.cipher.init(Cipher.ENCRYPT_MODE, this.key);
184       } else {
185         this.cipher.init(Cipher.DECRYPT_MODE, this.key);
186       }
187     } catch(Exception x) {
188       throw new RuntimeException("Failed to init cipher for "+(encrypt ? "encrypt" : "decrypt"), x);
189     }
190   }
191  
192   /**
193    * Get the encrypted input stream
194    * @param in
195    * @return the encrypted input stream
196    */
197   public InputStream encrypt(InputStream in) {
198     this.initCipher(true);
199     return new CipherInputStream(in, this.cipher);
200   }
201   
202   /**
203    * the decrypted input stream
204    * @param in
205    * @return the decrypted input stream
206    */
207   public InputStream decrypt(InputStream in) {
208     this.initCipher(false);
209     return new CipherInputStream(in, this.cipher);
210   }
211   
212   /**
213    * the encrypted output stream
214    * @param out
215    * @return the encrypted output stream
216    */
217   public OutputStream encrypt(OutputStream out) {
218     this.initCipher(true);
219     return new CipherOutputStream(out, this.cipher);
220   }
221   
222   /**
223    * the decrypted output stream
224    * @param out
225    * @return the decrypted output stream
226    */
227   public OutputStream decrypt(OutputStream out) {
228     this.initCipher(false);
229     return new CipherOutputStream(out, this.cipher);
230   }
231 }