View Javadoc
1   package org.apache.commons.jcs3.utils.serialization;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.nio.ByteBuffer;
24  import java.security.InvalidAlgorithmParameterException;
25  import java.security.InvalidKeyException;
26  import java.security.NoSuchAlgorithmException;
27  import java.security.SecureRandom;
28  import java.security.spec.InvalidKeySpecException;
29  
30  import javax.crypto.BadPaddingException;
31  import javax.crypto.Cipher;
32  import javax.crypto.IllegalBlockSizeException;
33  import javax.crypto.NoSuchPaddingException;
34  import javax.crypto.SecretKey;
35  import javax.crypto.SecretKeyFactory;
36  import javax.crypto.spec.GCMParameterSpec;
37  import javax.crypto.spec.PBEKeySpec;
38  import javax.crypto.spec.SecretKeySpec;
39  
40  import org.apache.commons.jcs3.engine.behavior.IElementSerializer;
41  
42  /**
43   * Performs serialization and de-serialization. It encrypts and decrypts the
44   * value.
45   *
46   * @since 3.1
47   */
48  public class EncryptingSerializer extends StandardSerializer
49  {
50      private static final String DEFAULT_SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA256";
51      private static final String DEFAULT_CIPHER = "AES/ECB/PKCS5Padding";
52      private static final int KEYHASH_ITERATION_COUNT = 1000;
53      private static final int KEY_LENGTH = 256;
54      private static final int TAG_LENGTH = 128;
55      private static final int IV_LENGTH = 12;
56      private static final int SALT_LENGTH = 16;
57  
58      /** The pre-shared key */
59      private String psk;
60  
61      /** The cipher transformation */
62      private String cipherTransformation = DEFAULT_CIPHER;
63  
64      /** The random source */
65      private final SecureRandom secureRandom;
66  
67      /** The secret-key factory */
68      private final SecretKeyFactory secretKeyFactory;
69  
70      /** Wrapped serializer */
71      private final IElementSerializer serializer;
72  
73      /**
74       * Default constructor
75       */
76      public EncryptingSerializer()
77      {
78          this(new StandardSerializer());
79      }
80  
81      /**
82       * Wrapper constructor
83       *
84       * @param serializer
85       *            the wrapped serializer
86       */
87      public EncryptingSerializer(IElementSerializer serializer)
88      {
89          this.serializer = serializer;
90  
91          try
92          {
93              this.secureRandom = new SecureRandom();
94              this.secretKeyFactory = SecretKeyFactory.getInstance(DEFAULT_SECRET_KEY_ALGORITHM);
95          }
96          catch (NoSuchAlgorithmException e)
97          {
98              throw new RuntimeException("Could not set up encryption tools", e);
99          }
100     }
101 
102     /**
103      * Set the pre-shared key for encryption and decryption
104      *
105      * @param psk the key
106      */
107     public void setPreSharedKey(String psk)
108     {
109         this.psk = psk;
110     }
111 
112     /**
113      * Set the cipher transformation for encryption and decryption
114      * Default is AES/ECB/PKCS5Padding
115      *
116      * @param transformation the transformation
117      */
118     public void setAesCipherTransformation(String transformation)
119     {
120         this.cipherTransformation = transformation;
121     }
122 
123     private byte[] getRandomBytes(int length)
124     {
125         byte[] bytes = new byte[length];
126         secureRandom.nextBytes(bytes);
127 
128         return bytes;
129     }
130 
131     private SecretKey createSecretKey(String password, byte[] salt) throws InvalidKeySpecException
132     {
133         /* Derive the key, given password and salt. */
134         PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt,
135                 KEYHASH_ITERATION_COUNT, KEY_LENGTH);
136         SecretKey tmp = secretKeyFactory.generateSecret(spec);
137         return new SecretKeySpec(tmp.getEncoded(), "AES");
138     }
139 
140     private byte[] encrypt(byte[] source) throws IOException
141     {
142         try
143         {
144             byte[] salt = getRandomBytes(SALT_LENGTH);
145             byte[] iv = getRandomBytes(IV_LENGTH);
146 
147             SecretKey secretKey = createSecretKey(psk, salt);
148             Cipher cipher = Cipher.getInstance(cipherTransformation);
149             if (cipher.getAlgorithm().startsWith("AES/GCM"))
150             {
151                 cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH, iv));
152             }
153             else
154             {
155                 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
156             }
157 
158             byte[] encrypted = cipher.doFinal(source);
159 
160             // join initial vector, salt and encrypted data for later decryption
161             return ByteBuffer.allocate(IV_LENGTH + SALT_LENGTH + encrypted.length)
162                     .put(iv)
163                     .put(salt)
164                     .put(encrypted)
165                     .array();
166         }
167         catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
168                 IllegalBlockSizeException | InvalidKeyException | InvalidKeySpecException |
169                 InvalidAlgorithmParameterException e)
170         {
171             throw new IOException("Error while encrypting", e);
172         }
173     }
174 
175     private byte[] decrypt(byte[] source) throws IOException
176     {
177         try
178         {
179             // split data in initial vector, salt and encrypted data
180             ByteBuffer wrapped = ByteBuffer.wrap(source);
181 
182             byte[] iv = new byte[IV_LENGTH];
183             wrapped.get(iv);
184 
185             byte[] salt = new byte[SALT_LENGTH];
186             wrapped.get(salt);
187 
188             byte[] encrypted = new byte[wrapped.remaining()];
189             wrapped.get(encrypted);
190 
191             SecretKey secretKey = createSecretKey(psk, salt);
192             Cipher cipher = Cipher.getInstance(cipherTransformation);
193 
194             if (cipher.getAlgorithm().startsWith("AES/GCM"))
195             {
196                 cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH, iv));
197             }
198             else
199             {
200                 cipher.init(Cipher.DECRYPT_MODE, secretKey);
201             }
202 
203             return cipher.doFinal(encrypted);
204         }
205         catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
206                 IllegalBlockSizeException | InvalidKeyException | InvalidKeySpecException |
207                 InvalidAlgorithmParameterException e)
208         {
209             throw new IOException("Error while decrypting", e);
210         }
211     }
212 
213     /**
214      * Serializes an object using default serialization. Encrypts the byte array.
215      * <p>
216      * @param obj object
217      * @return byte[]
218      * @throws IOException on i/o problem
219      */
220     @Override
221     public <T> byte[] serialize( final T obj )
222         throws IOException
223     {
224         final byte[] unencrypted = serializer.serialize(obj);
225         return encrypt(unencrypted);
226     }
227 
228     /**
229      * Uses default de-serialization to turn a byte array into an object. Decrypts the value
230      * first. All exceptions are converted into IOExceptions.
231      * <p>
232      * @param data data bytes
233      * @param loader class loader to use
234      * @return Object
235      * @throws IOException on i/o problem
236      * @throws ClassNotFoundException if class is not found during deserialization
237      */
238     @Override
239     public <T> T deSerialize( final byte[] data, final ClassLoader loader )
240         throws IOException, ClassNotFoundException
241     {
242         if ( data == null )
243         {
244             return null;
245         }
246 
247         final byte[] deccrypted = decrypt(data);
248         return serializer.deSerialize(deccrypted, loader);
249     }
250 }