001package org.apache.commons.jcs3.utils.serialization;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.IOException;
023import java.nio.ByteBuffer;
024import java.security.InvalidAlgorithmParameterException;
025import java.security.InvalidKeyException;
026import java.security.NoSuchAlgorithmException;
027import java.security.SecureRandom;
028import java.security.spec.InvalidKeySpecException;
029
030import javax.crypto.BadPaddingException;
031import javax.crypto.Cipher;
032import javax.crypto.IllegalBlockSizeException;
033import javax.crypto.NoSuchPaddingException;
034import javax.crypto.SecretKey;
035import javax.crypto.SecretKeyFactory;
036import javax.crypto.spec.GCMParameterSpec;
037import javax.crypto.spec.PBEKeySpec;
038import javax.crypto.spec.SecretKeySpec;
039
040import org.apache.commons.jcs3.engine.behavior.IElementSerializer;
041
042/**
043 * Performs serialization and de-serialization. It encrypts and decrypts the
044 * value.
045 *
046 * @since 3.1
047 */
048public class EncryptingSerializer extends StandardSerializer
049{
050    private static final String DEFAULT_SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA256";
051    private static final String DEFAULT_CIPHER = "AES/ECB/PKCS5Padding";
052    private static final int KEYHASH_ITERATION_COUNT = 1000;
053    private static final int KEY_LENGTH = 256;
054    private static final int TAG_LENGTH = 128;
055    private static final int IV_LENGTH = 12;
056    private static final int SALT_LENGTH = 16;
057
058    /** The pre-shared key */
059    private String psk;
060
061    /** The cipher transformation */
062    private String cipherTransformation = DEFAULT_CIPHER;
063
064    /** The random source */
065    private final SecureRandom secureRandom;
066
067    /** The secret-key factory */
068    private final SecretKeyFactory secretKeyFactory;
069
070    /** Wrapped serializer */
071    private final IElementSerializer serializer;
072
073    /**
074     * Default constructor
075     */
076    public EncryptingSerializer()
077    {
078        this(new StandardSerializer());
079    }
080
081    /**
082     * Wrapper constructor
083     *
084     * @param serializer
085     *            the wrapped serializer
086     */
087    public EncryptingSerializer(IElementSerializer serializer)
088    {
089        this.serializer = serializer;
090
091        try
092        {
093            this.secureRandom = new SecureRandom();
094            this.secretKeyFactory = SecretKeyFactory.getInstance(DEFAULT_SECRET_KEY_ALGORITHM);
095        }
096        catch (NoSuchAlgorithmException e)
097        {
098            throw new RuntimeException("Could not set up encryption tools", e);
099        }
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}