Skip to content

Commit

Permalink
Merge pull request #391 from PerimeterX/feature/multiple-cookies
Browse files Browse the repository at this point in the history
Multiple cookie secrets
  • Loading branch information
guyeisenbach authored Sep 12, 2024
2 parents c2970ff + 4598e1c commit 3920b98
Show file tree
Hide file tree
Showing 21 changed files with 316 additions and 104 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [vX.XX.X]() (XXX-XX-XX)
- Bump Fuzzer version
- Support multiple cookie secret rotation

## [v6.13.0](https://github.com/PerimeterX/perimeterx-java-sdk/compare/6.13.0...HEAD) (2024-04-27)
- Added vid Validation for _pxvid extraction
Expand Down
22 changes: 14 additions & 8 deletions src/main/java/com/perimeterx/api/PerimeterX.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import com.perimeterx.utils.logger.IPXLogger;
import com.perimeterx.utils.StringUtils;
import com.perimeterx.utils.logger.LoggerFactory;
import edu.emory.mathcs.backport.java.util.Collections;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponseWrapper;
Expand All @@ -68,8 +69,10 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.List;

import static com.perimeterx.utils.Constants.*;
import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;
import static java.util.Objects.isNull;

/**
Expand All @@ -80,7 +83,7 @@

public class PerimeterX implements Closeable {

public static IPXLogger globalLogger = LoggerFactory.getGlobalLogger();;
public static IPXLogger globalLogger = LoggerFactory.getGlobalLogger();
private PXConfiguration configuration;
private PXS2SValidator serverValidator;
private PXCookieValidator cookieValidator;
Expand Down Expand Up @@ -259,7 +262,7 @@ private void setAdditionalS2SActivityHeaders(HttpServletRequest request, PXConte

public void pxPostVerify(ResponseWrapper response, PXContext context) throws PXException {
try {
if (context != null){
if (context != null) {
if (response != null && !configuration.isAdditionalS2SActivityHeaderEnabled() && context.isContainCredentialsIntelligence()) {
handleAdditionalS2SActivityWithCI(response, context);
}
Expand Down Expand Up @@ -303,21 +306,24 @@ public boolean isValidTelemetryRequest(HttpServletRequest request, PXContext con
return false;
}

final byte[] hmacBytes = HMACUtils.HMACString(timestamp, configuration.getCookieKey());
final String generatedHmac = StringUtils.byteArrayToHexString(hmacBytes).toLowerCase();
for (String key : cookieKeysToCheck(context, configuration)) {
final byte[] hmacBytes = HMACUtils.HMACString(timestamp, key);
final String generatedHmac = StringUtils.byteArrayToHexString(hmacBytes).toLowerCase();

if (!MessageDigest.isEqual(generatedHmac.getBytes(), hmac.getBytes())) {
context.logger.error("Telemetry validation failed - invalid hmac, original=" + hmac + ", generated=" + generatedHmac);
return false;
if (MessageDigest.isEqual(generatedHmac.getBytes(), hmac.getBytes())) {
return true;
}
}
context.logger.debug("Telemetry validation failed - invalid hmac, original=" + hmac);
} catch (NoSuchAlgorithmException | InvalidKeyException | IllegalArgumentException e) {
context.logger.error("Telemetry validation failed.");
return false;
}

return true;
return false;
}


/**
* Set activity handler
*
Expand Down
50 changes: 31 additions & 19 deletions src/main/java/com/perimeterx/internals/cookie/AbstractPXCookie.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@
import com.perimeterx.utils.logger.LogReason;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;

/**
* Created by nitzangoldfeder on 13/04/2017.
*/
Expand All @@ -38,7 +42,6 @@ public abstract class AbstractPXCookie implements PXCookie {
protected PXConfiguration pxConfiguration;
protected String pxCookie;
protected JsonNode decodedCookie;
protected String cookieKey;
protected String cookieOrig;

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

Expand Down Expand Up @@ -81,7 +83,7 @@ public boolean deserialize() throws PXCookieDecryptionException {

JsonNode decodedCookie;
if (this.pxConfiguration.isEncryptionEnabled()) {
decodedCookie = this.decrypt();
decodedCookie = this.decrypt(context);
} else {
decodedCookie = this.decode();
}
Expand All @@ -94,7 +96,7 @@ public boolean deserialize() throws PXCookieDecryptionException {
return true;
}

private JsonNode decrypt() throws PXCookieDecryptionException {
private JsonNode decrypt(PXContext context) throws PXCookieDecryptionException {
final String[] parts = this.pxCookie.split(":");
if (parts.length != 3) {
throw new PXCookieDecryptionException("Part length invalid");
Expand All @@ -115,21 +117,30 @@ private JsonNode decrypt() throws PXCookieDecryptionException {
final Cipher cipher; // aes-256-cbc decryptData no salt
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
final int dkLen = KEY_LEN + cipher.getBlockSize();
PBKDF2Parameters p = new PBKDF2Parameters(HMAC_SHA_256, "UTF-8", salt, iterations);
byte[] dk = new PBKDF2Engine(p).deriveKey(this.cookieKey, dkLen);
byte[] key = Arrays.copyOf(dk, KEY_LEN);
byte[] iv = Arrays.copyOfRange(dk, KEY_LEN, dk.length);
SecretKey secretKey = new SecretKeySpec(key, "AES");
IvParameterSpec parameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
final byte[] data = cipher.doFinal(encrypted, 0, encrypted.length);

String decryptedString = new String(data, StandardCharsets.UTF_8);
return mapper.readTree(decryptedString);
} catch (Exception e) {
throw new PXCookieDecryptionException("Cookie decryption failed in reason => ".concat(e.getMessage()));
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new PXCookieDecryptionException(e);
}
final int dkLen = KEY_LEN + cipher.getBlockSize();
PBKDF2Parameters p = new PBKDF2Parameters(HMAC_SHA_256, "UTF-8", salt, iterations);

for (String cookieKey : this.pxConfiguration.getCookieKeys()) {
try {
byte[] dk = new PBKDF2Engine(p).deriveKey(cookieKey, dkLen);
byte[] key = Arrays.copyOf(dk, KEY_LEN);
byte[] iv = Arrays.copyOfRange(dk, KEY_LEN, dk.length);
SecretKey secretKey = new SecretKeySpec(key, "AES");
IvParameterSpec parameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
final byte[] data = cipher.doFinal(encrypted, 0, encrypted.length);

String decryptedString = new String(data, StandardCharsets.UTF_8);
JsonNode result = mapper.readTree(decryptedString);
context.setCookieKeyUsed(cookieKey);
return result;
} catch (Exception ignored) {
}
}
throw new PXCookieDecryptionException("Cookie decryption failed");
}

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

public boolean isHmacValid(String hmacStr, String cookieHmac) {
boolean isValid = HMACUtils.isHMACValid(hmacStr, cookieHmac, this.cookieKey, logger);
boolean isValid = cookieKeysToCheck(this.context, this.pxConfiguration).stream()
.anyMatch(cookieKey -> HMACUtils.isHMACValid(hmacStr, cookieHmac, cookieKey, logger));
if (!isValid) {
context.logger.debug(LogReason.DEBUG_COOKIE_DECRYPTION_HMAC_FAILED, pxCookie, this.userAgent);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.*;
import java.util.stream.Stream;

import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;
import static java.util.stream.Collectors.toList;

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

public DataEnrichmentCookie getRawDataEnrichmentCookie(List<RawCookieData> rawCookies, String cookieKey) {
return getRawDataEnrichmentCookie(rawCookies, Collections.singletonList(cookieKey));
}
public DataEnrichmentCookie getRawDataEnrichmentCookie(List<RawCookieData> rawCookies, List<String> cookieKeys) {
ObjectMapper mapper = new ObjectMapper();
DataEnrichmentCookie dataEnrichmentCookie = new DataEnrichmentCookie(mapper.createObjectNode(), false);
RawCookieData rawDataEnrichmentCookie = null;
Expand All @@ -67,7 +71,8 @@ public DataEnrichmentCookie getRawDataEnrichmentCookie(List<RawCookieData> rawCo
String hmac = cookiePayloadArray[0];
String encodedPayload = cookiePayloadArray[1];

boolean isValid = HMACUtils.isHMACValid(encodedPayload, hmac, cookieKey, logger);
boolean isValid = cookieKeys.stream()
.anyMatch(cookieKey -> HMACUtils.isHMACValid(encodedPayload, hmac, cookieKey, logger));
dataEnrichmentCookie.setValid(isValid);

byte[] decodedPayload = Base64.decode(encodedPayload);
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/com/perimeterx/models/PXContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import static com.perimeterx.utils.Constants.BREACHED_ACCOUNT_KEY_NAME;
import static com.perimeterx.utils.Constants.LOGGER_TOKEN_HEADER_NAME;
import static com.perimeterx.utils.PXCommonUtils.cookieHeadersNames;
import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck;

/**
* PXContext - Populate relevant data from HttpRequest
Expand Down Expand Up @@ -229,6 +230,11 @@ public class PXContext {
private String pxhdDomain;
private long enforcerStartTime;

/**
* The cookie key used to decrypt the cookie
*/
private String cookieKeyUsed;

/**
* The base64 encoded request full url
*/
Expand Down Expand Up @@ -394,7 +400,7 @@ private void parseCookies(HttpServletRequest request, boolean isMobileToken) {
setVidAndPxhd(cookies);
tokens.addAll(headerParser.createRawCookieDataList(cookieHeaders));
this.tokens = tokens;
DataEnrichmentCookie deCookie = headerParser.getRawDataEnrichmentCookie(this.tokens, this.pxConfiguration.getCookieKey());
DataEnrichmentCookie deCookie = headerParser.getRawDataEnrichmentCookie(this.tokens, cookieKeysToCheck(this, this.pxConfiguration));
this.pxde = deCookie.getJsonPayload();
this.pxdeVerified = deCookie.isValid();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.perimeterx.models.configuration;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.perimeterx.api.PerimeterX;
import com.perimeterx.api.additionalContext.credentialsIntelligence.CIProtocol;
Expand Down Expand Up @@ -31,11 +32,7 @@

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -66,7 +63,9 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) {
private String appId;

@JsonProperty("px_cookie_secret")
private String cookieKey;
@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
@Singular
private List<String> cookieKeys;

@JsonProperty("px_auth_token")
private String authToken;
Expand Down Expand Up @@ -313,7 +312,7 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) {
* @return Configuration Object clone without cookieKey and authToken
**/
public PXConfiguration getTelemetryConfig() {
return this.toBuilder().cookieKey(null).authToken(null).build();
return this.toBuilder().clearCookieKeys().authToken(null).build();
}

public void disableModule() {
Expand Down Expand Up @@ -365,12 +364,11 @@ public ReverseProxy getReverseProxyInstance() {
return reverseProxyInstance;
}


public void update(PXDynamicConfiguration pxDynamicConfiguration) {
PerimeterX.globalLogger.debug("Updating PXConfiguration file");
this.appId = pxDynamicConfiguration.getAppId();
this.checksum = pxDynamicConfiguration.getChecksum();
this.cookieKey = pxDynamicConfiguration.getCookieSecret();
this.cookieKeys = pxDynamicConfiguration.getCookieSecrets();
this.blockingScore = pxDynamicConfiguration.getBlockingScore();
this.apiTimeout = pxDynamicConfiguration.getApiConnectTimeout();
this.connectionTimeout = pxDynamicConfiguration.getApiConnectTimeout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;

import java.util.List;
import java.util.Set;

/**
Expand All @@ -16,7 +19,9 @@ public class PXDynamicConfiguration {
@JsonProperty("checksum")
private String checksum;
@JsonProperty("cookieKey")
private String cookieSecret;
@Getter
@Setter
private List<String> cookieSecrets;
@JsonProperty("appId")
private String appId;
@JsonProperty("blockingScore")
Expand Down Expand Up @@ -48,14 +53,6 @@ public void setChecksum(String checksum) {
this.checksum = checksum;
}

public String getCookieSecret() {
return cookieSecret;
}

public void setCookieSecret(String cookieSecert) {
this.cookieSecret = cookieSecert;
}

public String getAppId() {
return appId;
}
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/perimeterx/utils/PXCommonUtils.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.perimeterx.utils;

import com.perimeterx.models.PXContext;
import com.perimeterx.models.configuration.PXConfiguration;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
Expand All @@ -19,6 +20,14 @@
public class PXCommonUtils {
final static int FIRST_PARTY_CONNECTION_TIMEOUT_IN_MSEC = 4000;

public static List<String> cookieKeysToCheck(PXContext context, PXConfiguration configuration) {
if (context.getCookieKeyUsed() == null) {
return configuration.getCookieKeys();
}

return Collections.singletonList(context.getCookieKeyUsed());
}

public static List<Header> getDefaultHeaders(String authToken) {
Header contentType = new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json");
Header authorization = new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + authToken);
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/com/perimeterx/api/PerimeterXTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public void testTelemetryObject() {
Assert.assertTrue(!clonedConfig.equals(configuration));

Assert.assertTrue(!configuration.getAuthToken().equals(clonedConfig.getAuthToken()));
Assert.assertTrue(!configuration.getCookieKey().equals(clonedConfig.getCookieKey()));
Assert.assertTrue(!configuration.getCookieKeys().equals(clonedConfig.getCookieKeys()));

Assert.assertEquals(clonedConfig.getAppId(), configuration.getAppId());
Assert.assertEquals(clonedConfig.isModuleEnabled(), configuration.isModuleEnabled());
Expand Down
Loading

0 comments on commit 3920b98

Please sign in to comment.