Skip to content

Commit 904ef89

Browse files
authored
Merge pull request #393 from PerimeterX/release/v6.14.0
release 6.14.0 -> master
2 parents c48c451 + 353b52a commit 904ef89

26 files changed

Lines changed: 329 additions & 109 deletions

.github/workflows/fuzzer.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
name: "Fuzzing Test"
2727
env:
2828
MOCK_COLLECTOR_IMAGE_TAG: 1.3.6
29-
FUZZER_TAG: 1.0.3
29+
FUZZER_TAG: 1.0.4
3030
SAMPLE_SITE_IMAGE_TAG: 1.0.0
3131
ENFORCER_TAG: ${{ needs.extract_version.outputs.version }}
3232

@@ -147,6 +147,11 @@ jobs:
147147
PX_APP_ID: ${{ secrets.PX_APP_ID }}
148148
SITE_URL: "http://java-enforcer-sample-site:3000"
149149

150+
- name: Deployment logs - mock collector
151+
run: kubectl logs deployment/mock-collector-mock-collector
152+
- name: Deployment logs - enforcer
153+
run: kubectl logs deployment/java-enforcer-sample-site
154+
150155
- name: get tests results
151156
if: ${{ always() }}
152157
run: kubectl logs job/fuzzer-enforcer-fuzzer

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## [v6.14.0](https://github.com/PerimeterX/perimeterx-java-sdk/compare/6.14.0...HEAD) (2024-09-15)
4+
- Bump Fuzzer version
5+
- Support cookie secret rotation
6+
37
## [v6.13.0](https://github.com/PerimeterX/perimeterx-java-sdk/compare/6.13.0...HEAD) (2024-04-27)
48
- Added vid Validation for _pxvid extraction
59
- Added Enforcer Fuzzer as part of the CI process

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# [PerimeterX](http://www.perimeterx.com) Java SDK
66

7-
> Latest stable version: [v6.13.0](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.13.0%7Cjar)
7+
> Latest stable version: [v6.14.0](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.15.0%7Cjar)
88
99
## Table of Contents
1010

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<name>PerimeterX JAVA SDK</name>
88
<groupId>com.perimeterx</groupId>
99
<artifactId>perimeterx-sdk</artifactId>
10-
<version>6.13.0</version>
10+
<version>6.14.0</version>
1111

1212
<packaging>jar</packaging>
1313
<description>PerimeterX Java SDK</description>

px_metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "6.13.0",
2+
"version": "6.14.0",
33
"supported_features": [
44
"advanced_blocking_response",
55
"bypass_monitor_header",

src/main/java/com/perimeterx/api/PerimeterX.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import com.perimeterx.utils.logger.IPXLogger;
5959
import com.perimeterx.utils.StringUtils;
6060
import com.perimeterx.utils.logger.LoggerFactory;
61+
import edu.emory.mathcs.backport.java.util.Collections;
6162

6263
import javax.servlet.http.HttpServletRequest;
6364
import javax.servlet.http.HttpServletResponseWrapper;
@@ -68,8 +69,10 @@
6869
import java.security.MessageDigest;
6970
import java.security.NoSuchAlgorithmException;
7071
import java.util.Base64;
72+
import java.util.List;
7173

7274
import static com.perimeterx.utils.Constants.*;
75+
import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;
7376
import static java.util.Objects.isNull;
7477

7578
/**
@@ -80,7 +83,7 @@
8083

8184
public class PerimeterX implements Closeable {
8285

83-
public static IPXLogger globalLogger = LoggerFactory.getGlobalLogger();;
86+
public static IPXLogger globalLogger = LoggerFactory.getGlobalLogger();
8487
private PXConfiguration configuration;
8588
private PXS2SValidator serverValidator;
8689
private PXCookieValidator cookieValidator;
@@ -259,7 +262,7 @@ private void setAdditionalS2SActivityHeaders(HttpServletRequest request, PXConte
259262

260263
public void pxPostVerify(ResponseWrapper response, PXContext context) throws PXException {
261264
try {
262-
if (context != null){
265+
if (context != null) {
263266
if (response != null && !configuration.isAdditionalS2SActivityHeaderEnabled() && context.isContainCredentialsIntelligence()) {
264267
handleAdditionalS2SActivityWithCI(response, context);
265268
}
@@ -303,21 +306,24 @@ public boolean isValidTelemetryRequest(HttpServletRequest request, PXContext con
303306
return false;
304307
}
305308

306-
final byte[] hmacBytes = HMACUtils.HMACString(timestamp, configuration.getCookieKey());
307-
final String generatedHmac = StringUtils.byteArrayToHexString(hmacBytes).toLowerCase();
309+
for (String key : cookieKeysToCheck(context, configuration)) {
310+
final byte[] hmacBytes = HMACUtils.HMACString(timestamp, key);
311+
final String generatedHmac = StringUtils.byteArrayToHexString(hmacBytes).toLowerCase();
308312

309-
if (!MessageDigest.isEqual(generatedHmac.getBytes(), hmac.getBytes())) {
310-
context.logger.error("Telemetry validation failed - invalid hmac, original=" + hmac + ", generated=" + generatedHmac);
311-
return false;
313+
if (MessageDigest.isEqual(generatedHmac.getBytes(), hmac.getBytes())) {
314+
return true;
315+
}
312316
}
317+
context.logger.debug("Telemetry validation failed - invalid hmac, original=" + hmac);
313318
} catch (NoSuchAlgorithmException | InvalidKeyException | IllegalArgumentException e) {
314319
context.logger.error("Telemetry validation failed.");
315320
return false;
316321
}
317322

318-
return true;
323+
return false;
319324
}
320325

326+
321327
/**
322328
* Set activity handler
323329
*

src/main/java/com/perimeterx/internals/cookie/AbstractPXCookie.java

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111
import com.perimeterx.utils.logger.LogReason;
1212

1313
import javax.crypto.Cipher;
14+
import javax.crypto.NoSuchPaddingException;
1415
import javax.crypto.SecretKey;
1516
import javax.crypto.spec.IvParameterSpec;
1617
import javax.crypto.spec.SecretKeySpec;
1718
import java.io.IOException;
1819
import java.nio.charset.StandardCharsets;
20+
import java.security.NoSuchAlgorithmException;
1921
import java.util.Arrays;
2022

23+
import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;
24+
2125
/**
2226
* Created by nitzangoldfeder on 13/04/2017.
2327
*/
@@ -38,7 +42,6 @@ public abstract class AbstractPXCookie implements PXCookie {
3842
protected PXConfiguration pxConfiguration;
3943
protected String pxCookie;
4044
protected JsonNode decodedCookie;
41-
protected String cookieKey;
4245
protected String cookieOrig;
4346

4447
public AbstractPXCookie(PXConfiguration pxConfiguration, CookieData cookieData, PXContext context) {
@@ -49,7 +52,6 @@ public AbstractPXCookie(PXConfiguration pxConfiguration, CookieData cookieData,
4952
this.pxConfiguration = pxConfiguration;
5053
this.userAgent = cookieData.isMobileToken() ? "" : cookieData.getUserAgent();
5154
this.ip = cookieData.getIp();
52-
this.cookieKey = pxConfiguration.getCookieKey();
5355
this.cookieVersion = cookieData.getCookieVersion();
5456
}
5557

@@ -81,7 +83,7 @@ public boolean deserialize() throws PXCookieDecryptionException {
8183

8284
JsonNode decodedCookie;
8385
if (this.pxConfiguration.isEncryptionEnabled()) {
84-
decodedCookie = this.decrypt();
86+
decodedCookie = this.decrypt(context);
8587
} else {
8688
decodedCookie = this.decode();
8789
}
@@ -94,7 +96,7 @@ public boolean deserialize() throws PXCookieDecryptionException {
9496
return true;
9597
}
9698

97-
private JsonNode decrypt() throws PXCookieDecryptionException {
99+
private JsonNode decrypt(PXContext context) throws PXCookieDecryptionException {
98100
final String[] parts = this.pxCookie.split(":");
99101
if (parts.length != 3) {
100102
throw new PXCookieDecryptionException("Part length invalid");
@@ -115,21 +117,30 @@ private JsonNode decrypt() throws PXCookieDecryptionException {
115117
final Cipher cipher; // aes-256-cbc decryptData no salt
116118
try {
117119
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
118-
final int dkLen = KEY_LEN + cipher.getBlockSize();
119-
PBKDF2Parameters p = new PBKDF2Parameters(HMAC_SHA_256, "UTF-8", salt, iterations);
120-
byte[] dk = new PBKDF2Engine(p).deriveKey(this.cookieKey, dkLen);
121-
byte[] key = Arrays.copyOf(dk, KEY_LEN);
122-
byte[] iv = Arrays.copyOfRange(dk, KEY_LEN, dk.length);
123-
SecretKey secretKey = new SecretKeySpec(key, "AES");
124-
IvParameterSpec parameterSpec = new IvParameterSpec(iv);
125-
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
126-
final byte[] data = cipher.doFinal(encrypted, 0, encrypted.length);
127-
128-
String decryptedString = new String(data, StandardCharsets.UTF_8);
129-
return mapper.readTree(decryptedString);
130-
} catch (Exception e) {
131-
throw new PXCookieDecryptionException("Cookie decryption failed in reason => ".concat(e.getMessage()));
120+
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
121+
throw new PXCookieDecryptionException(e);
122+
}
123+
final int dkLen = KEY_LEN + cipher.getBlockSize();
124+
PBKDF2Parameters p = new PBKDF2Parameters(HMAC_SHA_256, "UTF-8", salt, iterations);
125+
126+
for (String cookieKey : this.pxConfiguration.getCookieKeys()) {
127+
try {
128+
byte[] dk = new PBKDF2Engine(p).deriveKey(cookieKey, dkLen);
129+
byte[] key = Arrays.copyOf(dk, KEY_LEN);
130+
byte[] iv = Arrays.copyOfRange(dk, KEY_LEN, dk.length);
131+
SecretKey secretKey = new SecretKeySpec(key, "AES");
132+
IvParameterSpec parameterSpec = new IvParameterSpec(iv);
133+
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
134+
final byte[] data = cipher.doFinal(encrypted, 0, encrypted.length);
135+
136+
String decryptedString = new String(data, StandardCharsets.UTF_8);
137+
JsonNode result = mapper.readTree(decryptedString);
138+
context.setCookieKeyUsed(cookieKey);
139+
return result;
140+
} catch (Exception ignored) {
141+
}
132142
}
143+
throw new PXCookieDecryptionException("Cookie decryption failed");
133144
}
134145

135146
private JsonNode decode() throws PXCookieDecryptionException {
@@ -152,7 +163,8 @@ public boolean isExpired() {
152163
}
153164

154165
public boolean isHmacValid(String hmacStr, String cookieHmac) {
155-
boolean isValid = HMACUtils.isHMACValid(hmacStr, cookieHmac, this.cookieKey, logger);
166+
boolean isValid = cookieKeysToCheck(this.context, this.pxConfiguration).stream()
167+
.anyMatch(cookieKey -> HMACUtils.isHMACValid(hmacStr, cookieHmac, cookieKey, logger));
156168
if (!isValid) {
157169
context.logger.debug(LogReason.DEBUG_COOKIE_DECRYPTION_HMAC_FAILED, pxCookie, this.userAgent);
158170
}

src/main/java/com/perimeterx/internals/cookie/cookieparsers/HeaderParser.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.*;
1717
import java.util.stream.Stream;
1818

19+
import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;
1920
import static java.util.stream.Collectors.toList;
2021

2122
public abstract class HeaderParser {
@@ -48,6 +49,9 @@ public List<RawCookieData> createRawCookieDataList(String... cookieHeaders) {
4849
}
4950

5051
public DataEnrichmentCookie getRawDataEnrichmentCookie(List<RawCookieData> rawCookies, String cookieKey) {
52+
return getRawDataEnrichmentCookie(rawCookies, Collections.singletonList(cookieKey));
53+
}
54+
public DataEnrichmentCookie getRawDataEnrichmentCookie(List<RawCookieData> rawCookies, List<String> cookieKeys) {
5155
ObjectMapper mapper = new ObjectMapper();
5256
DataEnrichmentCookie dataEnrichmentCookie = new DataEnrichmentCookie(mapper.createObjectNode(), false);
5357
RawCookieData rawDataEnrichmentCookie = null;
@@ -67,7 +71,8 @@ public DataEnrichmentCookie getRawDataEnrichmentCookie(List<RawCookieData> rawCo
6771
String hmac = cookiePayloadArray[0];
6872
String encodedPayload = cookiePayloadArray[1];
6973

70-
boolean isValid = HMACUtils.isHMACValid(encodedPayload, hmac, cookieKey, logger);
74+
boolean isValid = cookieKeys.stream()
75+
.anyMatch(cookieKey -> HMACUtils.isHMACValid(encodedPayload, hmac, cookieKey, logger));
7176
dataEnrichmentCookie.setValid(isValid);
7277

7378
byte[] decodedPayload = Base64.decode(encodedPayload);

src/main/java/com/perimeterx/models/PXContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import static com.perimeterx.utils.Constants.BREACHED_ACCOUNT_KEY_NAME;
4242
import static com.perimeterx.utils.Constants.LOGGER_TOKEN_HEADER_NAME;
4343
import static com.perimeterx.utils.PXCommonUtils.cookieHeadersNames;
44+
import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;
4445

4546
/**
4647
* PXContext - Populate relevant data from HttpRequest
@@ -229,6 +230,11 @@ public class PXContext {
229230
private String pxhdDomain;
230231
private long enforcerStartTime;
231232

233+
/**
234+
* The cookie key used to decrypt the cookie
235+
*/
236+
private String cookieKeyUsed;
237+
232238
/**
233239
* The base64 encoded request full url
234240
*/
@@ -394,7 +400,7 @@ private void parseCookies(HttpServletRequest request, boolean isMobileToken) {
394400
setVidAndPxhd(cookies);
395401
tokens.addAll(headerParser.createRawCookieDataList(cookieHeaders));
396402
this.tokens = tokens;
397-
DataEnrichmentCookie deCookie = headerParser.getRawDataEnrichmentCookie(this.tokens, this.pxConfiguration.getCookieKey());
403+
DataEnrichmentCookie deCookie = headerParser.getRawDataEnrichmentCookie(this.tokens, cookieKeysToCheck(this, this.pxConfiguration));
398404
this.pxde = deCookie.getJsonPayload();
399405
this.pxdeVerified = deCookie.isValid();
400406
}

src/main/java/com/perimeterx/models/configuration/PXConfiguration.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.perimeterx.models.configuration;
22

3+
import com.fasterxml.jackson.annotation.JsonFormat;
34
import com.fasterxml.jackson.annotation.JsonProperty;
45
import com.perimeterx.api.PerimeterX;
56
import com.perimeterx.api.additionalContext.credentialsIntelligence.CIProtocol;
@@ -31,11 +32,7 @@
3132

3233
import javax.servlet.http.HttpServletRequest;
3334
import java.io.IOException;
34-
import java.lang.reflect.Field;
35-
import java.util.Arrays;
36-
import java.util.HashSet;
37-
import java.util.Map;
38-
import java.util.Set;
35+
import java.util.*;
3936
import java.util.function.Function;
4037
import java.util.function.Predicate;
4138
import java.util.stream.Collectors;
@@ -66,7 +63,9 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) {
6663
private String appId;
6764

6865
@JsonProperty("px_cookie_secret")
69-
private String cookieKey;
66+
@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
67+
@Singular
68+
private List<String> cookieKeys;
7069

7170
@JsonProperty("px_auth_token")
7271
private String authToken;
@@ -313,7 +312,7 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) {
313312
* @return Configuration Object clone without cookieKey and authToken
314313
**/
315314
public PXConfiguration getTelemetryConfig() {
316-
return this.toBuilder().cookieKey(null).authToken(null).build();
315+
return this.toBuilder().clearCookieKeys().authToken(null).build();
317316
}
318317

319318
public void disableModule() {
@@ -365,12 +364,11 @@ public ReverseProxy getReverseProxyInstance() {
365364
return reverseProxyInstance;
366365
}
367366

368-
369367
public void update(PXDynamicConfiguration pxDynamicConfiguration) {
370368
PerimeterX.globalLogger.debug("Updating PXConfiguration file");
371369
this.appId = pxDynamicConfiguration.getAppId();
372370
this.checksum = pxDynamicConfiguration.getChecksum();
373-
this.cookieKey = pxDynamicConfiguration.getCookieSecret();
371+
this.cookieKeys = pxDynamicConfiguration.getCookieSecrets();
374372
this.blockingScore = pxDynamicConfiguration.getBlockingScore();
375373
this.apiTimeout = pxDynamicConfiguration.getApiConnectTimeout();
376374
this.connectionTimeout = pxDynamicConfiguration.getApiConnectTimeout();

0 commit comments

Comments
 (0)