Core Java

Auto-encrypting Serializable Classes

A crazy idea came up during the post-mortem discussions in the Coursera security capstone project. Can a class encrypt itself during serialization?

This is mostly an academic “what if” exercise. It is hard to think of a situation where we would want to rely on an object self-encrypting instead of using an explicit encryption mechanism during persistence. I’ve only been able to identify one situation where we can’t simply make a class impossible to serialize:

HTTPSession passivation

Appservers may passivate inactive HTTPSessions to save space or to migrate a session from one server to another. This is why sessions should only contain Serializable objects. (This restriction is often ignored in small-scale applications that can fit on a single server but that can cause problems if the implementation needs to be scaled up or out.)

One approach (and the preferred approach?) is for the session to write itself to a database during passivation and reload itself during activation. The only information actually retained is what’s require to reload the data, typically just the user id. This adds a bit of complexity to the HTTPSession implementation but it has many benefits. One major benefit is that it trivial to ensure sensitive information is encrypted.

It’s not the only approach and some sites may prefer to use standard serialization. Some appservers may keep copies of serialized copies of “live” sessions in an embedded database like H2. A cautious developer may want to ensure that sensitive information is encrypted during serialization even if it should never happen.

Note: a strong argument can be made that the sensitive information shouldn’t be in the session in the first place – only retrieve it when necessary and safely discard it once it is no longer needed.

The approach

The approach I’m taking is based on the serialization chapter in Effective Java. In broad terms we want to use a serialization proxy to handle the actual encryption. The behavior is:

ActionMethodProtected Serialized ClassSerialization Proxy
SerializationwriteReplace()create proxyN/A
writeObject()throw exceptionwrite encrypted contents to ObjectOutputStream
DeserializationreadObject()read encrypted contents from ObjectInputStream
readResolve()construct protected class object

 
The reason the protected class throws an exception when the deserialization methods are called is because it prevents attacks through attacker-generated serialized objects. See the discussion on the bogus byte-stream attack and internal field theft attack in the book mentioned above.

This approach has a big limitation – the class cannot be extended without the subclass reimplementing the proxy. I don’t think this is an issue in practice since this technique will only be used to protect classes containing sensitive information and it would rarely be desirable to add methods beyond the ones anticipated by the designers.

The proxy class handles encryption. The implementation below shows the use of a random salt (IV) and cryptographically strong message digest (HMAC) to detect tampering.

The code

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
public class ProtectedSecret implements Serializable {
    private static final long serialVersionUID = 1L;
 
    private final String secret;
 
    /**
     * Constructor.
     *
     * @param secret
     */
    public ProtectedSecret(final String secret) {
        this.secret = secret;
    }
 
    /**
     * Accessor
     */
    public String getSecret() {
        return secret;
    }
 
    /**
     * Replace the object being serialized with a proxy.
     *
     * @return
     */
    private Object writeReplace() {
        return new SimpleProtectedSecretProxy(this);
    }
 
    /**
     * Serialize object. We throw an exception since this method should never be
     * called - the standard serialization engine will serialize the proxy
     * returned by writeReplace(). Anyone calling this method directly is
     * probably up to no good.
     *
     * @param stream
     * @return
     * @throws InvalidObjectException
     */
    private void writeObject(ObjectOutputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
 
    /**
     * Deserialize object. We throw an exception since this method should never
     * be called - the standard serialization engine will create serialized
     * proxies instead. Anyone calling this method directly is probably up to no
     * good and using a manually constructed serialized object.
     *
     * @param stream
     * @return
     * @throws InvalidObjectException
     */
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
 
    /**
     * Serializable proxy for our protected class. The encryption code is based
     */
    private static class SimpleProtectedSecretProxy implements Serializable {
        private static final long serialVersionUID = 1L;
        private String secret;
 
        private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
        private static final String HMAC_ALGORITHM = "HmacSHA256";
 
        private static transient SecretKeySpec cipherKey;
        private static transient SecretKeySpec hmacKey;
 
        static {
            // these keys can be read from the environment, the filesystem, etc.
            final byte[] aes_key = "d2cb415e067c7b13".getBytes();
            final byte[] hmac_key = "d6cfaad283353507".getBytes();
 
            try {
                cipherKey = new SecretKeySpec(aes_key, "AES");
                hmacKey = new SecretKeySpec(hmac_key, HMAC_ALGORITHM);
            } catch (Exception e) {
                throw new ExceptionInInitializerError(e);
            }
        }
 
        /**
         * Constructor.
         *
         * @param protectedSecret
         */
        SimpleProtectedSecretProxy(ProtectedSecret protectedSecret) {
            this.secret = protectedSecret.secret;
        }
 
        /**
         * Write encrypted object to serialization stream.
         *
         * @param s
         * @throws IOException
         */
        private void writeObject(ObjectOutputStream s) throws IOException {
            s.defaultWriteObject();
            try {
                Cipher encrypt = Cipher.getInstance(CIPHER_ALGORITHM);
                encrypt.init(Cipher.ENCRYPT_MODE, cipherKey);
                byte[] ciphertext = encrypt.doFinal(secret.getBytes("UTF-8"));
                byte[] iv = encrypt.getIV();
 
                Mac mac = Mac.getInstance(HMAC_ALGORITHM);
                mac.init(hmacKey);
                mac.update(iv);
                byte[] hmac = mac.doFinal(ciphertext);
 
                // TBD: write algorithm id...
                s.writeInt(iv.length);
                s.write(iv);
                s.writeInt(ciphertext.length);
                s.write(ciphertext);
                s.writeInt(hmac.length);
                s.write(hmac);
            } catch (Exception e) {
                throw new InvalidObjectException("unable to encrypt value");
            }
        }
 
        /**
         * Read encrypted object from serialization stream.
         *
         * @param s
         * @throws InvalidObjectException
         */
        private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException, InvalidObjectException {
            s.defaultReadObject();
            try {
                // TBD: read algorithm id...
                byte[] iv = new byte[s.readInt()];
                s.read(iv);
                byte[] ciphertext = new byte[s.readInt()];
                s.read(ciphertext);
                byte[] hmac = new byte[s.readInt()];
                s.read(hmac);
 
                // verify HMAC
                Mac mac = Mac.getInstance(HMAC_ALGORITHM);
                mac.init(hmacKey);
                mac.update(iv);
                byte[] signature = mac.doFinal(ciphertext);
 
                // verify HMAC
                if (!Arrays.equals(hmac, signature)) {
                    throw new InvalidObjectException("unable to decrypt value");
                }
 
                // decrypt data
                Cipher decrypt = Cipher.getInstance(CIPHER_ALGORITHM);
                decrypt.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(iv));
                byte[] data = decrypt.doFinal(ciphertext);
                secret = new String(data, "UTF-8");
            } catch (Exception e) {
                throw new InvalidObjectException("unable to decrypt value");
            }
        }
 
        /**
         * Return protected object.
         *
         * @return
         */
        private Object readResolve() {
            return new ProtectedSecret(secret);
        }
    }
}

It should go without saying that the encryption keys should not be hard-coded or possibly even cached as shown. This was a short-cut to allow us to focus on the details of the implementation.

Different keys should be used for the cipher and message digest. You will seriously compromise the security of your system if the same key is used.

Two other things should be handled in any production system: key rotation and changing the cipher and digest algorithms. The former can be handled by adding a ‘key id’ to the payload, the latter can be handled by tying the serialization version number and cipher algorithms. E.g., version 1 uses standard AES, version 2 uses AES-256. The deserializer should be able to handle old encryption keys and ciphers (within reason).

Test code

The test code is straightforward. It creates an object, serializes it, deserializes it, and compares the results to the original value.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class ProtectedSecretTest {
 
    /**
     * Test 'happy path'.
     */
    @Test
    public void testCipher() throws IOException, ClassNotFoundException {
        ProtectedSecret secret1 = new ProtectedSecret("password");
        ProtectedSecret secret2;
        byte[] ser;
 
        // serialize object
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutput output = new ObjectOutputStream(baos)) {
            output.writeObject(secret1);
            output.flush();
 
            ser = baos.toByteArray();
        }
 
        // deserialize object.
        try (ByteArrayInputStream bais = new ByteArrayInputStream(ser); ObjectInput input = new ObjectInputStream(bais)) {
            secret2 = (ProtectedSecret) input.readObject();
        }
 
        // compare values.
        assertEquals(secret1.getSecret(), secret2.getSecret());
    }
 
    /**
     * Test deserialization after a single bit is flipped.
     */
    @Test(expected = InvalidObjectException.class)
    public void testCipherAltered() throws IOException, ClassNotFoundException {
        ProtectedSecret secret1 = new ProtectedSecret("password");
        ProtectedSecret secret2;
        byte[] ser;
 
        // serialize object
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutput output = new ObjectOutputStream(baos)) {
            output.writeObject(secret1);
            output.flush();
 
            ser = baos.toByteArray();
        }
         
        // corrupt ciphertext
        ser[ser.length - 16 - 1 - 3] ^= 1;
 
        // deserialize object.
        try (ByteArrayInputStream bais = new ByteArrayInputStream(ser); ObjectInput input = new ObjectInputStream(bais)) {
            secret2 = (ProtectedSecret) input.readObject();
        }
 
        // compare values.
        assertEquals(secret1.getSecret(), secret2.getSecret());
    }
}

Final words

I cannot overemphasize this – this is primarily an intellectual exercise. As usual the biggest problem is key management, not cryptography, and with the level of effort required for the former you can probably implement a more traditional solution more quickly.

This may still be “good enough” in some situations. For instance you may only need to keep the data around during the duration of a long-running application. In this case you can create random keys at startup and simply discard all serialized data after the program ends.

Reference: Auto-encrypting Serializable Classes from our JCG partner Bear Giles at the Invariant Properties blog.
Do you want to know how to develop your skillset to become a Java Rockstar?
Subscribe to our newsletter to start Rocking right now!
To get you started we give you our best selling eBooks for FREE!
1. JPA Mini Book
2. JVM Troubleshooting Guide
3. JUnit Tutorial for Unit Testing
4. Java Annotations Tutorial
5. Java Interview Questions
6. Spring Interview Questions
7. Android UI Design
and many more ....
I agree to the Terms and Privacy Policy
Subscribe
Notify of
guest


This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button