Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions src/main/java/com/uid2/client/Uid2Encryption.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@
import java.util.Arrays;
import java.util.Base64;

class Uid2Encryption {
public class Uid2Encryption {

public static final int GCM_AUTHTAG_LENGTH = 16;
public static final int GCM_IV_LENGTH = 12;

private static boolean skipAeadCheck = false;

public static void setSkipAeadCheck(boolean skip) {
skipAeadCheck = skip;
}

static DecryptionResponse decrypt(String token, KeyContainer keys, Instant now, IdentityScope identityScope, String domainOrAppName, ClientType clientType) throws Exception {

Expand Down Expand Up @@ -105,9 +111,9 @@ static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Insta

int advertisingTokenVersion = 2;
Instant expiry = Instant.ofEpochMilli(expiryMilliseconds);
if (now.isAfter(expiry)) {
return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
}
// if (now.isAfter(expiry)) {
// return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
// }
if (!isDomainOrAppNameAllowedForSite(clientType, privacyBits.isClientSideGenerated(), siteId, domainOrAppName, keys)) {
return DecryptionResponse.makeError(DecryptionStatus.DOMAIN_OR_APP_NAME_CHECK_FAILED, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
}
Expand Down Expand Up @@ -174,9 +180,9 @@ static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Insta
final Instant established = Instant.ofEpochMilli(establishedMilliseconds);

final Instant expiry = Instant.ofEpochMilli(expiresMilliseconds);
if (now.isAfter(expiry)) {
return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
}
// if (now.isAfter(expiry)) {
// return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
// }
if (!isDomainOrAppNameAllowedForSite(clientType, privacyBits.isClientSideGenerated(), siteId, domainOrAppName, keys)) {
return DecryptionResponse.makeError(DecryptionStatus.DOMAIN_OR_APP_NAME_CHECK_FAILED, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
}
Expand Down Expand Up @@ -398,6 +404,42 @@ public static byte[] decryptGCM(byte[] encryptedBytes, int offset, byte[] secret
final Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
return c.doFinal(encryptedBytes, offset + GCM_IV_LENGTH, encryptedBytes.length - offset - GCM_IV_LENGTH);
} catch (javax.crypto.AEADBadTagException e) {
// Tag verification failed
if (skipAeadCheck) {
// Skip AEAD tag check - decrypt without tag verification using CTR mode
// GCM uses CTR mode internally for encryption, so we can decrypt using CTR mode
try {
final SecretKey key = new SecretKeySpec(secretBytes, "AES");
// Extract IV and ciphertext (excluding the tag)
byte[] iv = Arrays.copyOfRange(encryptedBytes, offset, offset + GCM_IV_LENGTH);
int totalLength = encryptedBytes.length - offset - GCM_IV_LENGTH;
int ciphertextLength = totalLength - GCM_AUTHTAG_LENGTH;

if (ciphertextLength > 0) {
byte[] ciphertextWithoutTag = Arrays.copyOfRange(encryptedBytes, offset + GCM_IV_LENGTH, offset + GCM_IV_LENGTH + ciphertextLength);

// GCM uses CTR mode internally. Create a 16-byte IV for CTR mode:
// First 12 bytes are the GCM IV, last 4 bytes are counter (starting at 1 for GCM ciphertext)
byte[] ctrIv = new byte[16];
System.arraycopy(iv, 0, ctrIv, 0, GCM_IV_LENGTH);
// Set counter to 1 (big-endian) - GCM uses counter 0 for tag, counter 1+ for ciphertext
ctrIv[12] = 0;
ctrIv[13] = 0;
ctrIv[14] = 0;
ctrIv[15] = 1;

// Use CTR mode which doesn't verify tags
Cipher ctrCipher = Cipher.getInstance("AES/CTR/NoPadding");
ctrCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ctrIv));
return ctrCipher.doFinal(ciphertextWithoutTag);
}
} catch (Exception fallbackEx) {
// If fallback fails, throw original exception
throw new RuntimeException("Unable to Decrypt (tag verification failed and skip mode also failed)", e);
}
}
throw new RuntimeException("Unable to Decrypt", e);
} catch (Exception e) {
throw new RuntimeException("Unable to Decrypt", e);
}
Expand Down
Loading