Skip to content

Commit 856bf91

Browse files
authored
Merge pull request #2748 from vert-x3/backport/2746
Ensuring that cookie sessions are reusable across nodes
2 parents 1cef082 + b449b84 commit 856bf91

File tree

5 files changed

+200
-115
lines changed

5 files changed

+200
-115
lines changed

vertx-web-session-stores/vertx-web-sstore-cookie/src/main/java/io/vertx/ext/web/sstore/cookie/CookieSessionStore.java

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,48 @@
2323

2424
/**
2525
* A SessionStore that uses a Cookie to store the session data. All data is stored in
26-
* encrypted form using {@code AES-256 with AES/CBC/PKCS5Padding}.
26+
* encrypted form using {@code AES-256 with AES/GCM/NoPadding}.
2727
*
2828
* @author <a href="mailto:plopes@redhat.com">Paulo Lopes</a>
2929
*/
3030
@VertxGen
3131
public interface CookieSessionStore extends SessionStore {
3232

3333
/**
34+
* @deprecated use {@link #create(Vertx, String)}
35+
*
3436
* Creates a CookieSessionStore.
37+
*
38+
* This factory method is deprecated and will be removed in a future version.
39+
* The salt value is ignored and should not be used. This was an artifact of
40+
* the original implementation which used a different encryption scheme.
3541
*
36-
* Cookie data will be encrypted using the given secret and salt. The secret as the name
42+
* @param vertx a vert.x instance
43+
* @param secret a secret to derive a secure private key
44+
* @param salt ignored
45+
* @return the store
46+
*/
47+
@Deprecated
48+
static CookieSessionStore create(Vertx vertx, String secret, Buffer salt) {
49+
return create(vertx, secret);
50+
}
51+
52+
/**
53+
* Creates a CookieSessionStore.
54+
*
55+
* Cookie data will be encrypted using the given secret. The secret as the name
3756
* reflects, should never leave the server, otherwise user agents could tamper
38-
* with the payload. The salt adds an extra later of security and should be a random.
57+
* with the payload.
58+
*
59+
* The choice of GCM, ensures that no (IV, Key) is reusable, which means that
60+
* there is no need for a salt. Also encrypting the same session multiple times
61+
* will render different outputs, which prevents rainbow attacks.
3962
*
4063
* @param vertx a vert.x instance
4164
* @param secret a secret to derive a secure private key
42-
* @param salt a binary salt used in the key derivation
4365
* @return the store
4466
*/
45-
static CookieSessionStore create(Vertx vertx, String secret, Buffer salt) {
46-
return new CookieSessionStoreImpl(vertx, secret, salt);
67+
static CookieSessionStore create(Vertx vertx, String secret) {
68+
return new CookieSessionStoreImpl(vertx, secret);
4769
}
4870
}

vertx-web-session-stores/vertx-web-sstore-cookie/src/main/java/io/vertx/ext/web/sstore/cookie/impl/CookieSession.java

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@
2222
import javax.crypto.BadPaddingException;
2323
import javax.crypto.Cipher;
2424
import javax.crypto.IllegalBlockSizeException;
25+
import javax.crypto.NoSuchPaddingException;
26+
import javax.crypto.spec.GCMParameterSpec;
27+
import javax.crypto.spec.SecretKeySpec;
2528
import java.nio.charset.Charset;
2629
import java.nio.charset.StandardCharsets;
30+
import java.security.NoSuchAlgorithmException;
31+
import java.security.InvalidAlgorithmParameterException;
32+
import java.security.InvalidKeyException;
2733
import java.util.Base64;
2834

2935
/**
@@ -34,6 +40,11 @@ public class CookieSession extends AbstractSession {
3440
private static final Base64.Encoder BASE64_URL_ENCODER = Base64.getUrlEncoder().withoutPadding();
3541
private static final Base64.Decoder BASE64_URL_DECODER = Base64.getUrlDecoder();
3642

43+
private static final String AES_ALGORITHM_GCM = "AES/GCM/NoPadding";
44+
45+
private static final int IV_LENGTH = 12;
46+
private static final int TAG_LENGTH = 16;
47+
3748
public static String base64UrlEncode(byte[] bytes) {
3849
return BASE64_URL_ENCODER.encodeToString(bytes);
3950
}
@@ -44,23 +55,23 @@ public static byte[] base64UrlDecode(String base64) {
4455

4556
private static final Charset UTF8 = StandardCharsets.UTF_8;
4657

47-
private final Cipher encrypt;
48-
private final Cipher decrypt;
58+
private final SecretKeySpec aesKey;
59+
private final VertxContextPRNG prng;
4960
// track the original version
5061
private int oldVersion = 0;
5162
// track the original crc
5263
private int oldCrc = 0;
5364

54-
public CookieSession(Cipher encrypt, Cipher decrypt, VertxContextPRNG prng, long timeout, int length) {
65+
public CookieSession(SecretKeySpec aesKey, VertxContextPRNG prng, long timeout, int length) {
5566
super(prng, timeout, length);
56-
this.encrypt = encrypt;
57-
this.decrypt = decrypt;
67+
this.prng = prng;
68+
this.aesKey = aesKey;
5869
}
5970

60-
public CookieSession(Cipher encrypt, Cipher decrypt, VertxContextPRNG prng) {
71+
public CookieSession(SecretKeySpec aesKey, VertxContextPRNG prng) {
6172
super(prng);
62-
this.encrypt = encrypt;
63-
this.decrypt = decrypt;
73+
this.prng = prng;
74+
this.aesKey = aesKey;
6475
}
6576

6677
@Override
@@ -76,8 +87,8 @@ public String value() {
7687
writeDataToBuffer(buff);
7788

7889
try {
79-
return base64UrlEncode(encrypt.doFinal(buff.getBytes()));
80-
} catch (IllegalBlockSizeException | BadPaddingException e) {
90+
return encrypt(buff);
91+
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
8192
throw new RuntimeException(e);
8293
}
8394
}
@@ -99,21 +110,8 @@ protected CookieSession setValue(String payload) {
99110
throw new NullPointerException();
100111
}
101112

102-
// String[] tokens = payload.split("\\.");
103-
// if (tokens.length != 2) {
104-
// // no signature present, force a regeneration
105-
// // by claiming this session as invalid
106-
// return null;
107-
// }
108-
//
109-
// String signature = base64UrlEncode(mac.doFinal(tokens[0].getBytes(StandardCharsets.US_ASCII)));
110-
//
111-
// if(!signature.equals(tokens[1])) {
112-
// throw new RuntimeException("Session data was Tampered!");
113-
// }
114-
115113
try {
116-
final Buffer buffer = Buffer.buffer(decrypt.doFinal(base64UrlDecode(payload)));
114+
final Buffer buffer = decrypt(payload);
117115

118116
// reconstruct the session
119117
int pos = 0;
@@ -133,7 +131,7 @@ protected CookieSession setValue(String payload) {
133131
// defaults
134132
oldVersion = version();
135133
oldCrc = crc();
136-
} catch (IllegalBlockSizeException | BadPaddingException e) {
134+
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
137135
// this is a bad session, force a regeneration
138136
return null;
139137
}
@@ -144,4 +142,43 @@ protected CookieSession setValue(String payload) {
144142
int oldVersion() {
145143
return oldVersion;
146144
}
145+
146+
private String encrypt(Buffer data) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
147+
// Initialization Vector
148+
byte[] iv = new byte[IV_LENGTH];
149+
prng.nextBytes(iv);
150+
151+
// get a cipher
152+
Cipher cipher = Cipher.getInstance(AES_ALGORITHM_GCM);
153+
GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
154+
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
155+
156+
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
157+
158+
// combine IV and cipher data
159+
byte[] combinedIvAndCipherText = new byte[iv.length + encryptedBytes.length];
160+
System.arraycopy(iv, 0, combinedIvAndCipherText, 0, iv.length);
161+
System.arraycopy(encryptedBytes, 0, combinedIvAndCipherText, iv.length, encryptedBytes.length);
162+
163+
return base64UrlEncode(combinedIvAndCipherText);
164+
}
165+
166+
private Buffer decrypt(String data) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
167+
byte[] decodedCipherText = base64UrlDecode(data);
168+
169+
// extract the IV
170+
byte[] iv = new byte[IV_LENGTH];
171+
System.arraycopy(decodedCipherText, 0, iv, 0, iv.length);
172+
byte[] encryptedText = new byte[decodedCipherText.length - IV_LENGTH];
173+
System.arraycopy(decodedCipherText, IV_LENGTH, encryptedText, 0, encryptedText.length);
174+
175+
// get a cipher
176+
Cipher cipher = Cipher.getInstance(AES_ALGORITHM_GCM);
177+
GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
178+
cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec);
179+
180+
byte[] decryptedBytes = cipher.doFinal(encryptedText);
181+
182+
return Buffer.buffer(decryptedBytes);
183+
}
147184
}

vertx-web-session-stores/vertx-web-sstore-cookie/src/main/java/io/vertx/ext/web/sstore/cookie/impl/CookieSessionStoreImpl.java

Lines changed: 16 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,45 +18,36 @@
1818
import io.vertx.codegen.annotations.Nullable;
1919
import io.vertx.core.Future;
2020
import io.vertx.core.Vertx;
21-
import io.vertx.core.buffer.Buffer;
2221
import io.vertx.core.internal.ContextInternal;
2322
import io.vertx.core.json.JsonObject;
2423
import io.vertx.ext.auth.prng.VertxContextPRNG;
2524
import io.vertx.ext.web.Session;
2625
import io.vertx.ext.web.sstore.SessionStore;
2726
import io.vertx.ext.web.sstore.cookie.CookieSessionStore;
2827

29-
import javax.crypto.Cipher;
30-
import javax.crypto.NoSuchPaddingException;
31-
import javax.crypto.SecretKey;
32-
import javax.crypto.SecretKeyFactory;
33-
import javax.crypto.spec.IvParameterSpec;
34-
import javax.crypto.spec.PBEKeySpec;
3528
import javax.crypto.spec.SecretKeySpec;
36-
import java.security.InvalidAlgorithmParameterException;
37-
import java.security.InvalidKeyException;
29+
import java.nio.charset.StandardCharsets;
30+
import java.security.MessageDigest;
3831
import java.security.NoSuchAlgorithmException;
39-
import java.security.spec.InvalidKeySpecException;
40-
import java.security.spec.KeySpec;
4132
import java.util.Objects;
4233

4334
/**
4435
* @author <a href="mailto:plopes@redhat.com">Paulo Lopes</a>
4536
*/
4637
public class CookieSessionStoreImpl implements CookieSessionStore {
4738

39+
private static final String SHA_CRYPT = "SHA-256";
40+
private static final String AES_ALGORITHM = "AES";
41+
4842
public CookieSessionStoreImpl() {
4943
// required for the service loader
5044
}
5145

52-
public CookieSessionStoreImpl(Vertx vertx, String secret, Buffer salt) {
53-
init(vertx, new JsonObject()
54-
.put("secret", secret)
55-
.put("salt", salt));
46+
public CookieSessionStoreImpl(Vertx vertx, String secret) {
47+
init(vertx, new JsonObject().put("secret", secret));
5648
}
5749

58-
private Cipher encrypt;
59-
private Cipher decrypt;
50+
private SecretKeySpec aesKey;
6051
private VertxContextPRNG random;
6152
private ContextInternal ctx;
6253

@@ -67,38 +58,13 @@ public SessionStore init(Vertx vertx, JsonObject options) {
6758
this.ctx = (ContextInternal) vertx.getOrCreateContext();
6859

6960
Objects.requireNonNull(options.getValue("secret"), "secret must be set");
70-
Objects.requireNonNull(options.getValue("salt"), "salt must be set");
7161

7262
try {
73-
byte[] iv = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
74-
if (options.containsKey("iv")) {
75-
byte[] tmp = options.getBinary("iv");
76-
for (int i = 0; i < tmp.length && i < iv.length; i++) {
77-
iv[i] = tmp[i];
78-
}
79-
} else {
80-
random.nextBytes(iv);
81-
}
82-
IvParameterSpec ivspec = new IvParameterSpec(iv);
83-
84-
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
85-
KeySpec spec = new PBEKeySpec(
86-
options.getString("secret").toCharArray(),
87-
options.getBinary("salt"),
88-
65536,
89-
256);
90-
91-
SecretKey tmp = factory.generateSecret(spec);
92-
SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
93-
94-
encrypt = Cipher.getInstance("AES/CBC/PKCS5Padding");
95-
encrypt.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
96-
97-
decrypt = Cipher.getInstance("AES/CBC/PKCS5Padding");
98-
decrypt.init(Cipher.DECRYPT_MODE, secretKey, ivspec);
99-
100-
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException |
101-
InvalidAlgorithmParameterException | NoSuchPaddingException e) {
63+
// AES Key generation
64+
MessageDigest sha256 = MessageDigest.getInstance(SHA_CRYPT);
65+
byte[] keyBytes = sha256.digest(options.getString("secret").getBytes(StandardCharsets.UTF_8));
66+
aesKey = new SecretKeySpec(keyBytes, AES_ALGORITHM);
67+
} catch (NoSuchAlgorithmException e) {
10268
throw new RuntimeException(e);
10369
}
10470

@@ -112,18 +78,18 @@ public long retryTimeout() {
11278

11379
@Override
11480
public Session createSession(long timeout) {
115-
return new CookieSession(encrypt, decrypt, random, timeout, DEFAULT_SESSIONID_LENGTH);
81+
return new CookieSession(aesKey, random, timeout, DEFAULT_SESSIONID_LENGTH);
11682
}
11783

11884
@Override
11985
public Session createSession(long timeout, int length) {
120-
return new CookieSession(encrypt, decrypt, random, timeout, length);
86+
return new CookieSession(aesKey, random, timeout, length);
12187
}
12288

12389
@Override
12490
public Future<@Nullable Session> get(String cookieValue) {
12591
try {
126-
Session session = new CookieSession(encrypt, decrypt, random).setValue(cookieValue);
92+
Session session = new CookieSession(aesKey, random).setValue(cookieValue);
12793

12894
if (session == null) {
12995
return ctx.succeededFuture();

vertx-web-session-stores/vertx-web-sstore-cookie/src/test/java/io/vertx/ext/web/sstore/cookie/tests/CookieSessionCreateTest.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,9 @@ public class CookieSessionCreateTest {
3737
public RunTestOnContext rule = new RunTestOnContext();
3838

3939
@Test
40-
public void testCreateStoreAll() throws Exception {
41-
SessionStore store = SessionStore.create(rule.vertx(), new JsonObject().put("secret", "KeyboardCat!").put("salt", "salt").put("iv", "iv"));
42-
assertNotNull(store);
43-
assertTrue(store instanceof CookieSessionStore);
44-
}
45-
46-
@Test
47-
public void testCreateStoreNoIV() throws Exception {
48-
SessionStore store = SessionStore.create(rule.vertx(), new JsonObject().put("secret", "KeyboardCat!").put("salt", "salt"));
49-
assertNotNull(store);
50-
assertTrue(store instanceof CookieSessionStore);
51-
}
52-
53-
@Test
54-
public void testCreateStoreNoSalt() throws Exception {
40+
public void testCreateStore() throws Exception {
5541
SessionStore store = SessionStore.create(rule.vertx(), new JsonObject().put("secret", "KeyboardCat!"));
5642
assertNotNull(store);
57-
// salt is missing, default to LocalStore as we don't want to leak session data if config isn't complete
58-
assertFalse(store instanceof CookieSessionStore);
43+
assertTrue(store instanceof CookieSessionStore);
5944
}
6045
}

0 commit comments

Comments
 (0)