From 31cb31455ba7f4fd65ef3fb59d4ffb1092d8f04c Mon Sep 17 00:00:00 2001 From: sophia chen Date: Fri, 1 Aug 2025 17:22:49 +1000 Subject: [PATCH 01/38] added UID v4 generation functions --- .../operator/model/IdentityEnvironment.java | 20 ++++++ .../uid2/operator/model/IdentityVersion.java | 19 ++++++ .../com/uid2/operator/service/TokenUtils.java | 17 +++-- .../uid2/operator/service/V4TokenUtils.java | 67 +++++++++++++++++++ 4 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/uid2/operator/model/IdentityEnvironment.java create mode 100644 src/main/java/com/uid2/operator/model/IdentityVersion.java create mode 100644 src/main/java/com/uid2/operator/service/V4TokenUtils.java diff --git a/src/main/java/com/uid2/operator/model/IdentityEnvironment.java b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java new file mode 100644 index 000000000..bf462912b --- /dev/null +++ b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java @@ -0,0 +1,20 @@ +package com.uid2.operator.model; + +import com.uid2.operator.vertx.ClientInputValidationException; + +public enum IdentityEnvironment { + Test(0), Integ(1), Prod(2); + + public final int value; + + IdentityEnvironment(int value) { this.value = value; } + + public static IdentityEnvironment fromValue(int value) { + return switch (value) { + case 0 -> Test; + case 1 -> Integ; + case 2 -> Prod; + default -> throw new ClientInputValidationException("Invalid valid for IdentityEnvironment: " + value); + }; + } +} diff --git a/src/main/java/com/uid2/operator/model/IdentityVersion.java b/src/main/java/com/uid2/operator/model/IdentityVersion.java new file mode 100644 index 000000000..995071027 --- /dev/null +++ b/src/main/java/com/uid2/operator/model/IdentityVersion.java @@ -0,0 +1,19 @@ +package com.uid2.operator.model; + +import com.uid2.operator.vertx.ClientInputValidationException; + +public enum IdentityVersion { + V3(0), V4(1); + + public final int value; + + IdentityVersion(int value) { this.value = value; } + + public static IdentityVersion fromValue(int value) { + return switch (value) { + case 0 -> V3; + case 1 -> V4; + default -> throw new ClientInputValidationException("Invalid valid for IdentityVersion: " + value); + }; + } +} diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index 2cabc641b..bbf6f363d 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -1,10 +1,10 @@ package com.uid2.operator.service; +import com.uid2.operator.model.IdentityEnvironment; import com.uid2.operator.model.IdentityScope; import com.uid2.operator.model.IdentityType; - -import java.util.HashSet; -import java.util.Set; +import com.uid2.operator.model.IdentityVersion; +import com.uid2.shared.model.SaltEntry; public class TokenUtils { public static byte[] getIdentityHash(String identityString) { @@ -55,11 +55,18 @@ public static byte[] getAdvertisingIdV3FromIdentityHash(IdentityScope scope, Ide return getAdvertisingIdV3(scope, type, getFirstLevelHashFromIdentityHash(identityString, firstLevelSalt), rotatingSalt); } - public static byte encodeIdentityScope(IdentityScope identityScope) { - return (byte) (identityScope.value << 4); + public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte[] firstLevelHash, SaltEntry.KeyMaterial encryptingKey, String rotatingSalt) throws Exception { + byte metadata = (byte) (encodeIdentityVersion(IdentityVersion.V4) | encodeIdentityScope(scope) | encodeIdentityType(type) | encodeIdentityEnvironment(environment)); + return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), rotatingSalt); } + public static byte encodeIdentityScope(IdentityScope identityScope) { return (byte) (identityScope.value << 4); } + public static byte encodeIdentityType(IdentityType identityType) { return (byte) (identityType.value << 2); } + + public static byte encodeIdentityVersion(IdentityVersion identityVersion) { return (byte) (identityVersion.value << 6); } + + public static byte encodeIdentityEnvironment(IdentityEnvironment identityEnvironment) { return (byte) (identityEnvironment.value); } } diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java new file mode 100644 index 000000000..fc57e8806 --- /dev/null +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -0,0 +1,67 @@ +package com.uid2.operator.service; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import io.vertx.core.buffer.Buffer; +import java.util.Arrays; + +public class V4TokenUtils { + public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte metadata, int keyId) { + int iv_length = 12; + String iv_base = salt + .concat(Arrays.toString(firstLevelHashLast16Bytes)) + .concat(Byte.toString(metadata)) + .concat(String.valueOf(keyId)); + return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(iv_base), 0, iv_length); + } + + private static byte[] padIV16Bytes(byte[] iv) { + // Pad the 12-byte IV to 16 bytes for AES-CTR (standard block size) + byte[] paddedIV = new byte[16]; + System.arraycopy(iv, 0, paddedIV, 0, 12); + // Remaining 4 bytes are already zero-initialized (counter starts at 0) + return paddedIV; + } + + private static byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception { + // Set up AES256-CTR cipher + Cipher aesCtr = Cipher.getInstance("AES/CTR/NoPadding"); + SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(), "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(padIV16Bytes(iv)); + + aesCtr.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); + return aesCtr.doFinal(hash); + } + + public static byte generateChecksum(byte[] data) { + // Simple XOR checksum of all bytes + byte checksum = 0; + for (byte b : data) { + checksum ^= b; + } + System.out.println("Checksum: 0x" + String.format("%02X", checksum)); + return checksum; + } + + public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, int keyId, String key, String salt) throws Exception { + byte[] hash16Bytes = Arrays.copyOfRange(firstLevelHash, 0, 16); + byte[] iv = V4TokenUtils.generateIV(salt, hash16Bytes, metadata, keyId); + byte[] encryptedFirstLevelHash = V4TokenUtils.encryptHash(key, hash16Bytes, iv); + + Buffer buffer = Buffer.buffer(); + buffer.appendByte(metadata); + buffer.appendBytes(new byte[] { + (byte) (keyId & 0xFF), // LSB + (byte) ((keyId >> 8) & 0xFF), // Middle + (byte) ((keyId >> 16) & 0xFF) // MSB + }); + buffer.appendBytes(iv); + buffer.appendBytes(encryptedFirstLevelHash); + + byte checksum = generateChecksum(buffer.getBytes()); + buffer.appendByte(checksum); + + return buffer.getBytes(); + } +} From 59d7c0886575db0e690f789740ab770df0667c97 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Wed, 13 Aug 2025 18:16:40 +0800 Subject: [PATCH 02/38] Added UID V4 unit test and fixed FLH bug --- .../uid2/operator/service/V4TokenUtils.java | 74 ++++++++++--------- .../operator/service/V4TokenUtilsTest.java | 45 +++++++++++ 2 files changed, 86 insertions(+), 33 deletions(-) create mode 100644 src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java index fc57e8806..986ea9a28 100644 --- a/src/main/java/com/uid2/operator/service/V4TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -4,27 +4,48 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import io.vertx.core.buffer.Buffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.Arrays; -public class V4TokenUtils { +public final class V4TokenUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(V4TokenUtils.class); + private static final int IV_LENGTH = 12; + + private V4TokenUtils() { + } + + public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, int keyId, String key, String salt) throws Exception { + byte[] firstLevelHashLast16Bytes = Arrays.copyOfRange(firstLevelHash, firstLevelHash.length - 16, firstLevelHash.length); + byte[] iv = V4TokenUtils.generateIV(salt, firstLevelHashLast16Bytes, metadata, keyId); + byte[] encryptedFirstLevelHash = V4TokenUtils.encryptHash(key, firstLevelHashLast16Bytes, iv); + + Buffer buffer = Buffer.buffer(); + buffer.appendByte(metadata); + buffer.appendBytes(new byte[] { + (byte) (keyId & 0xFF), // LSB + (byte) ((keyId >> 8) & 0xFF), // Middle + (byte) ((keyId >> 16) & 0xFF) // MSB + }); + buffer.appendBytes(iv); + buffer.appendBytes(encryptedFirstLevelHash); + + byte checksum = generateChecksum(buffer.getBytes()); + buffer.appendByte(checksum); + + return buffer.getBytes(); + } + public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte metadata, int keyId) { - int iv_length = 12; - String iv_base = salt + String ivBase = salt .concat(Arrays.toString(firstLevelHashLast16Bytes)) .concat(Byte.toString(metadata)) .concat(String.valueOf(keyId)); - return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(iv_base), 0, iv_length); - } - - private static byte[] padIV16Bytes(byte[] iv) { - // Pad the 12-byte IV to 16 bytes for AES-CTR (standard block size) - byte[] paddedIV = new byte[16]; - System.arraycopy(iv, 0, paddedIV, 0, 12); - // Remaining 4 bytes are already zero-initialized (counter starts at 0) - return paddedIV; + return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(ivBase), 0, IV_LENGTH); } - private static byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception { + public static byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception { // Set up AES256-CTR cipher Cipher aesCtr = Cipher.getInstance("AES/CTR/NoPadding"); SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(), "AES"); @@ -40,28 +61,15 @@ public static byte generateChecksum(byte[] data) { for (byte b : data) { checksum ^= b; } - System.out.println("Checksum: 0x" + String.format("%02X", checksum)); + LOGGER.debug("Checksum: 0x{}", String.format("%02X", checksum)); return checksum; } - public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, int keyId, String key, String salt) throws Exception { - byte[] hash16Bytes = Arrays.copyOfRange(firstLevelHash, 0, 16); - byte[] iv = V4TokenUtils.generateIV(salt, hash16Bytes, metadata, keyId); - byte[] encryptedFirstLevelHash = V4TokenUtils.encryptHash(key, hash16Bytes, iv); - - Buffer buffer = Buffer.buffer(); - buffer.appendByte(metadata); - buffer.appendBytes(new byte[] { - (byte) (keyId & 0xFF), // LSB - (byte) ((keyId >> 8) & 0xFF), // Middle - (byte) ((keyId >> 16) & 0xFF) // MSB - }); - buffer.appendBytes(iv); - buffer.appendBytes(encryptedFirstLevelHash); - - byte checksum = generateChecksum(buffer.getBytes()); - buffer.appendByte(checksum); - - return buffer.getBytes(); + private static byte[] padIV16Bytes(byte[] iv) { + // Pad the 12-byte IV to 16 bytes for AES-CTR (standard block size) + byte[] paddedIV = new byte[16]; + System.arraycopy(iv, 0, paddedIV, 0, 12); + // Remaining 4 bytes are already zero-initialized (counter starts at 0) + return paddedIV; } } diff --git a/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java b/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java new file mode 100644 index 000000000..08e2f3c9e --- /dev/null +++ b/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java @@ -0,0 +1,45 @@ +package com.uid2.operator.service; + +import com.uid2.shared.model.SaltEntry; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static com.uid2.operator.service.V4TokenUtils.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class V4TokenUtilsTest { + @Test + void testBuildAdvertisingIdV4() throws Exception { + SaltEntry.KeyMaterial encryptionKey = new SaltEntry.KeyMaterial( + 1000000, + "key12345key12345key12345key12345", + "salt1234salt1234salt1234salt1234" + ); + byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity("test@example.com", encryptionKey.salt()); + byte metadata = (byte) 0b10110101; + byte[] v4UID = buildAdvertisingIdV4(metadata, firstLevelHash, encryptionKey.id(), encryptionKey.key(), encryptionKey.salt()); + assertEquals(33, v4UID.length); + + byte[] firstLevelHashLast16Bytes = Arrays.copyOfRange(firstLevelHash, firstLevelHash.length - 16, firstLevelHash.length); + byte[] iv = generateIV(encryptionKey.salt(), firstLevelHashLast16Bytes, metadata, encryptionKey.id()); + byte[] encryptedFirstLevelHash = encryptHash(encryptionKey.key(), firstLevelHashLast16Bytes, iv); + + byte extractedMetadata = v4UID[0]; + byte[] keyIdBytes = Arrays.copyOfRange(v4UID, 1, 4); + int extractedKeyId = (keyIdBytes[0] & 0xFF) | ((keyIdBytes[1] & 0xFF) << 8) | ((keyIdBytes[2] & 0xFF) << 16); + byte[] extractedIV = Arrays.copyOfRange(v4UID, 4, 16); + byte[] extractedEncryptedHash = Arrays.copyOfRange(v4UID, 16, 32); + byte extractedChecksum = v4UID[32]; + + assertEquals(metadata, extractedMetadata); + assertEquals(encryptionKey.id(), extractedKeyId); + assertArrayEquals(iv, extractedIV); + assertArrayEquals(encryptedFirstLevelHash, extractedEncryptedHash); + + // Verify checksum + byte recomputedChecksum = generateChecksum(Arrays.copyOfRange(v4UID, 0, 32)); + assertEquals(extractedChecksum, recomputedChecksum); + } +} From 78dc5fc000604b110b3df6d316ba9c6c5ae2ad6e Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Wed, 20 Aug 2025 16:41:37 +0800 Subject: [PATCH 03/38] Updated test --- src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java b/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java index 08e2f3c9e..c7f060a5a 100644 --- a/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java +++ b/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java @@ -18,7 +18,7 @@ void testBuildAdvertisingIdV4() throws Exception { "salt1234salt1234salt1234salt1234" ); byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity("test@example.com", encryptionKey.salt()); - byte metadata = (byte) 0b10110101; + byte metadata = (byte) 0b00100000; byte[] v4UID = buildAdvertisingIdV4(metadata, firstLevelHash, encryptionKey.id(), encryptionKey.key(), encryptionKey.salt()); assertEquals(33, v4UID.length); From 934701124aa934a784b467216d2e02fa94b97336 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 21 Aug 2025 16:48:26 +0800 Subject: [PATCH 04/38] Updated raw UID2 version generation logic --- .../operator/model/IdentityEnvironment.java | 13 +- .../com/uid2/operator/model/UserIdentity.java | 7 +- .../service/EncryptedTokenEncoder.java | 25 ++- .../operator/service/IUIDOperatorService.java | 3 - .../com/uid2/operator/service/InputUtil.java | 34 ++-- .../com/uid2/operator/service/TokenUtils.java | 11 +- .../operator/service/UIDOperatorService.java | 87 +++++---- .../uid2/operator/service/V4TokenUtils.java | 4 - .../operator/vertx/UIDOperatorVerticle.java | 61 +++---- .../com/uid2/operator/TokenEncodingTest.java | 43 +++-- .../uid2/operator/UIDOperatorServiceTest.java | 142 ++++++++------- .../operator/UIDOperatorVerticleTest.java | 170 +++++++++--------- .../operator/benchmark/BenchmarkCommon.java | 40 +++-- 13 files changed, 335 insertions(+), 305 deletions(-) diff --git a/src/main/java/com/uid2/operator/model/IdentityEnvironment.java b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java index bf462912b..209215798 100644 --- a/src/main/java/com/uid2/operator/model/IdentityEnvironment.java +++ b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java @@ -7,7 +7,9 @@ public enum IdentityEnvironment { public final int value; - IdentityEnvironment(int value) { this.value = value; } + IdentityEnvironment(int value) { + this.value = value; + } public static IdentityEnvironment fromValue(int value) { return switch (value) { @@ -17,4 +19,13 @@ public static IdentityEnvironment fromValue(int value) { default -> throw new ClientInputValidationException("Invalid valid for IdentityEnvironment: " + value); }; } + + public static IdentityEnvironment fromString(String value) { + return switch (value.toLowerCase()) { + case "test" -> Test; + case "integ" -> Integ; + case "prod" -> Prod; + default -> throw new ClientInputValidationException("Invalid valid for IdentityEnvironment: " + value); + }; + } } diff --git a/src/main/java/com/uid2/operator/model/UserIdentity.java b/src/main/java/com/uid2/operator/model/UserIdentity.java index 760d0ffb6..3529ce008 100644 --- a/src/main/java/com/uid2/operator/model/UserIdentity.java +++ b/src/main/java/com/uid2/operator/model/UserIdentity.java @@ -2,20 +2,22 @@ import java.time.Instant; import java.util.Arrays; -import java.util.Objects; public class UserIdentity { public final IdentityScope identityScope; public final IdentityType identityType; + public final IdentityEnvironment identityEnvironment; public final byte[] id; public final int privacyBits; public final Instant establishedAt; public final Instant refreshedAt; - public UserIdentity(IdentityScope identityScope, IdentityType identityType, byte[] id, int privacyBits, + public UserIdentity(IdentityScope identityScope, IdentityType identityType, IdentityEnvironment identityEnvironment, + byte[] id, int privacyBits, Instant establishedAt, Instant refreshedAt) { this.identityScope = identityScope; this.identityType = identityType; + this.identityEnvironment = identityEnvironment; this.id = id; this.privacyBits = privacyBits; this.establishedAt = establishedAt; @@ -25,6 +27,7 @@ public UserIdentity(IdentityScope identityScope, IdentityType identityType, byte public boolean matches(UserIdentity that) { return this.identityScope.equals(that.identityScope) && this.identityType.equals(that.identityType) && + this.identityEnvironment.equals(that.identityEnvironment) && Arrays.equals(this.id, that.id); } } diff --git a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java index f151f0919..e4d6763fe 100644 --- a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java +++ b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java @@ -17,9 +17,11 @@ public class EncryptedTokenEncoder implements ITokenEncoder { private final KeyManager keyManager; + private final IdentityEnvironment identityEnvironment; - public EncryptedTokenEncoder(KeyManager keyManager) { + public EncryptedTokenEncoder(KeyManager keyManager, IdentityEnvironment identityEnvironment) { this.keyManager = keyManager; + this.identityEnvironment = identityEnvironment; } public byte[] encode(AdvertisingToken t, Instant asOf) { @@ -124,7 +126,7 @@ private RefreshToken decodeRefreshTokenV2(Buffer b) { TokenVersion.V2, createdAt, validTill, new OperatorIdentity(0, OperatorType.Service, 0, 0), new PublisherIdentity(siteId, 0, 0), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, identity, privacyBits, Instant.ofEpochMilli(establishedMillis), null)); + new UserIdentity(IdentityScope.UID2, IdentityType.Email, this.identityEnvironment, identity, privacyBits, Instant.ofEpochMilli(establishedMillis), null)); } private RefreshToken decodeRefreshTokenV3(Buffer b, byte[] bytes) { @@ -157,7 +159,7 @@ private RefreshToken decodeRefreshTokenV3(Buffer b, byte[] bytes) { return new RefreshToken( TokenVersion.V3, createdAt, expiresAt, operatorIdentity, publisherIdentity, - new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, null)); + new UserIdentity(identityScope, identityType, this.identityEnvironment, id, privacyBits, establishedAt, null)); } @Override @@ -225,13 +227,12 @@ public AdvertisingToken decodeAdvertisingTokenV2(Buffer b) { Instant.ofEpochMilli(expiresMillis), new OperatorIdentity(0, OperatorType.Service, 0, masterKeyId), new PublisherIdentity(siteId, siteKeyId, 0), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null) + new UserIdentity(IdentityScope.UID2, IdentityType.Email, this.identityEnvironment, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null) ); } catch (Exception e) { throw new RuntimeException("Couldn't decode advertisingTokenV2", e); } - } public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, TokenVersion tokenVersion) { @@ -253,8 +254,7 @@ public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, Tok final IdentityScope identityScope = id.length == 32 ? IdentityScope.UID2 : decodeIdentityScopeV3(id[0]); final IdentityType identityType = id.length == 32 ? IdentityType.Email : decodeIdentityTypeV3(id[0]); - if (id.length > 32) - { + if (id.length > 32) { if (identityScope != decodeIdentityScopeV3(b.getByte(0))) { throw new ClientInputValidationException("Failed decoding advertisingTokenV3: Identity scope mismatch"); } @@ -265,7 +265,7 @@ public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, Tok return new AdvertisingToken( tokenVersion, createdAt, expiresAt, operatorIdentity, publisherIdentity, - new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, refreshedAt) + new UserIdentity(identityScope, identityType, this.identityEnvironment, id, privacyBits, establishedAt, refreshedAt) ); } @@ -338,7 +338,6 @@ public static String bytesToBase64Token(byte[] advertisingTokenBytes, TokenVersi @Override public IdentityTokens encode(AdvertisingToken advertisingToken, RefreshToken refreshToken, Instant refreshFrom, Instant asOf) { - final byte[] advertisingTokenBytes = encode(advertisingToken, asOf); final String base64AdvertisingToken = bytesToBase64Token(advertisingTokenBytes, advertisingToken.version); @@ -367,16 +366,16 @@ private byte[] encryptIdentityV2(PublisherIdentity publisherIdentity, UserIdenti } } - static private byte encodeIdentityTypeV3(UserIdentity userIdentity) { + private static byte encodeIdentityTypeV3(UserIdentity userIdentity) { return (byte) (TokenUtils.encodeIdentityScope(userIdentity.identityScope) | (userIdentity.identityType.value << 2) | 3); // "| 3" is used so that the 2nd char matches the version when V3 or higher. Eg "3" for V3 and "4" for V4 } - static private IdentityScope decodeIdentityScopeV3(byte value) { + private static IdentityScope decodeIdentityScopeV3(byte value) { return IdentityScope.fromValue((value & 0x10) >> 4); } - static private IdentityType decodeIdentityTypeV3(byte value) { + private static IdentityType decodeIdentityTypeV3(byte value) { return IdentityType.fromValue((value & 0xf) >> 2); } @@ -392,7 +391,7 @@ static PublisherIdentity decodePublisherIdentityV3(Buffer b, int offset) { static void encodeOperatorIdentityV3(Buffer b, OperatorIdentity operatorIdentity) { b.appendInt(operatorIdentity.siteId); - b.appendByte((byte)operatorIdentity.operatorType.value); + b.appendByte((byte) operatorIdentity.operatorType.value); b.appendInt(operatorIdentity.operatorVersion); b.appendInt(operatorIdentity.operatorKeyId); } diff --git a/src/main/java/com/uid2/operator/service/IUIDOperatorService.java b/src/main/java/com/uid2/operator/service/IUIDOperatorService.java index e824805f1..cf4f84abf 100644 --- a/src/main/java/com/uid2/operator/service/IUIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/IUIDOperatorService.java @@ -10,7 +10,6 @@ import java.util.List; public interface IUIDOperatorService { - IdentityTokens generateIdentity(IdentityRequest request, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter); RefreshResponse refreshIdentity(RefreshToken token, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter); @@ -25,6 +24,4 @@ public interface IUIDOperatorService { void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, Handler> handler); boolean advertisingTokenMatches(String advertisingToken, UserIdentity userIdentity, Instant asOf); - - Instant getLatestOptoutEntry(UserIdentity userIdentity, Instant asOf); } diff --git a/src/main/java/com/uid2/operator/service/InputUtil.java b/src/main/java/com/uid2/operator/service/InputUtil.java index 839e4e0f3..47c075534 100644 --- a/src/main/java/com/uid2/operator/service/InputUtil.java +++ b/src/main/java/com/uid2/operator/service/InputUtil.java @@ -1,17 +1,19 @@ package com.uid2.operator.service; +import com.uid2.operator.model.IdentityEnvironment; import com.uid2.operator.model.IdentityScope; import com.uid2.operator.model.IdentityType; import com.uid2.operator.model.UserIdentity; import java.time.Instant; -public class InputUtil { +public final class InputUtil { + private static final String GMAILDOMAIN = "gmail.com"; + private static final int MIN_PHONENUMBER_DIGITS = 10; + private static final int MAX_PHONENUMBER_DIGITS = 15; - private static String GMAILDOMAIN = "gmail.com"; - - private static int MIN_PHONENUMBER_DIGITS = 10; - private static int MAX_PHONENUMBER_DIGITS = 15; + private InputUtil() { + } public static InputVal normalizeEmailHash(String input) { final int inputLength = input.length(); @@ -65,8 +67,7 @@ public static boolean isPhoneNumberNormalized(String phoneNumber) { // count the digits, return false if non-digit character is found int totalDigits = 0; - for (int i = 1; i < phoneNumber.length(); ++i) - { + for (int i = 1; i < phoneNumber.length(); ++i) { if (!InputUtil.isAsciiDigit(phoneNumber.charAt(i))) return false; ++totalDigits; @@ -80,7 +81,7 @@ public static boolean isPhoneNumberNormalized(String phoneNumber) { public static InputVal normalizeEmail(String email) { final String normalize = normalizeEmailString(email); - if (normalize != null && normalize.length() > 0) { + if (normalize != null && !normalize.isEmpty()) { return InputVal.validEmail(email, normalize); } return InputVal.invalidEmail(email); @@ -143,15 +144,15 @@ public static String normalizeEmailString(String email) { wsBuffer.append(c); break; } - if (wsBuffer.length() > 0) { - sb.append(wsBuffer.toString()); + if (!wsBuffer.isEmpty()) { + sb.append(wsBuffer); wsBuffer = new StringBuilder(); } sb.append(c); } } } - if (sb.length() == 0) { + if (sb.isEmpty()) { return null; } final String domainPart = sb.toString(); @@ -162,7 +163,7 @@ public static String normalizeEmailString(String email) { } else { addressPartToUse = preSb; } - if (addressPartToUse.length() == 0) { + if (addressPartToUse.isEmpty()) { return null; } @@ -174,7 +175,7 @@ public enum IdentityInputType { Hash } - private static enum EmailParsingState { + private enum EmailParsingState { Starting, Pre, SubDomain, @@ -255,16 +256,19 @@ public IdentityType getIdentityType() { return identityType; } - public IdentityInputType getInputType() { return inputType; } + public IdentityInputType getInputType() { + return inputType; + } public boolean isValid() { return valid; } - public UserIdentity toUserIdentity(IdentityScope identityScope, int privacyBits, Instant establishedAt) { + public UserIdentity toUserIdentity(IdentityScope identityScope, IdentityEnvironment identityEnvironment, int privacyBits, Instant establishedAt) { return new UserIdentity( identityScope, this.identityType, + identityEnvironment, getIdentityInput(), privacyBits, establishedAt, diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index bbf6f363d..527ef5a53 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -6,7 +6,10 @@ import com.uid2.operator.model.IdentityVersion; import com.uid2.shared.model.SaltEntry; -public class TokenUtils { +public final class TokenUtils { + private TokenUtils() { + } + public static byte[] getIdentityHash(String identityString) { return EncodingUtils.getSha256Bytes(identityString); } @@ -42,7 +45,7 @@ public static byte[] getAdvertisingIdV2FromIdentityHash(String identityString, S public static byte[] getAdvertisingIdV3(IdentityScope scope, IdentityType type, byte[] firstLevelHash, String rotatingSalt) { final byte[] sha = EncodingUtils.getSha256Bytes(EncodingUtils.toBase64String(firstLevelHash), rotatingSalt); final byte[] id = new byte[33]; - id[0] = (byte)(encodeIdentityScope(scope) | encodeIdentityType(type)); + id[0] = (byte) (encodeIdentityScope(scope) | encodeIdentityType(type)); System.arraycopy(sha, 0, id, 1, 32); return id; } @@ -55,9 +58,9 @@ public static byte[] getAdvertisingIdV3FromIdentityHash(IdentityScope scope, Ide return getAdvertisingIdV3(scope, type, getFirstLevelHashFromIdentityHash(identityString, firstLevelSalt), rotatingSalt); } - public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte[] firstLevelHash, SaltEntry.KeyMaterial encryptingKey, String rotatingSalt) throws Exception { + public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte[] firstLevelHash, SaltEntry.KeyMaterial encryptingKey) throws Exception { byte metadata = (byte) (encodeIdentityVersion(IdentityVersion.V4) | encodeIdentityScope(scope) | encodeIdentityType(type) | encodeIdentityEnvironment(environment)); - return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), rotatingSalt); + return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), encryptingKey.salt()); } public static byte encodeIdentityScope(IdentityScope identityScope) { return (byte) (identityScope.value << 4); } diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index af422ad57..bd3a7b2fe 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -38,6 +38,7 @@ public class UIDOperatorService implements IUIDOperatorService { private final ITokenEncoder encoder; private final Clock clock; private final IdentityScope identityScope; + private final IdentityEnvironment identityEnvironment; private final UserIdentity testOptOutIdentityForEmail; private final UserIdentity testOptOutIdentityForPhone; private final UserIdentity testValidateIdentityForEmail; @@ -54,26 +55,28 @@ public class UIDOperatorService implements IUIDOperatorService { private final UidInstanceIdProvider uidInstanceIdProvider; public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, - IdentityScope identityScope, Handler saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) { + IdentityScope identityScope, IdentityEnvironment identityEnvironment, + Handler saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) { this.saltProvider = saltProvider; this.encoder = encoder; this.optOutStore = optOutStore; this.clock = clock; this.identityScope = identityScope; + this.identityEnvironment = identityEnvironment; this.saltRetrievalResponseHandler = saltRetrievalResponseHandler; this.uidInstanceIdProvider = uidInstanceIdProvider; - this.testOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, + this.testOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, identityEnvironment, InputUtil.normalizeEmail(OptOutIdentityForEmail).getIdentityInput(), Instant.now()); - this.testOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, + this.testOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, identityEnvironment, InputUtil.normalizePhone(OptOutIdentityForPhone).getIdentityInput(), Instant.now()); - this.testValidateIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, + this.testValidateIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, identityEnvironment, InputUtil.normalizeEmail(ValidateIdentityForEmail).getIdentityInput(), Instant.now()); - this.testValidateIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, + this.testValidateIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, identityEnvironment, InputUtil.normalizePhone(ValidateIdentityForPhone).getIdentityInput(), Instant.now()); - this.testRefreshOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, + this.testRefreshOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, identityEnvironment, InputUtil.normalizeEmail(RefreshOptOutIdentityForEmail).getIdentityInput(), Instant.now()); - this.testRefreshOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, + this.testRefreshOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, identityEnvironment, InputUtil.normalizePhone(RefreshOptOutIdentityForPhone).getIdentityInput(), Instant.now()); this.operatorIdentity = new OperatorIdentity(0, OperatorType.Service, 0, 0); @@ -100,8 +103,10 @@ public IdentityTokens generateIdentity(IdentityRequest request, Duration refresh final Instant now = EncodingUtils.NowUTCMillis(this.clock); final byte[] firstLevelHash = getFirstLevelHash(request.userIdentity.id, now); final UserIdentity firstLevelHashIdentity = new UserIdentity( - request.userIdentity.identityScope, request.userIdentity.identityType, firstLevelHash, request.userIdentity.privacyBits, - request.userIdentity.establishedAt, request.userIdentity.refreshedAt); + request.userIdentity.identityScope, request.userIdentity.identityType, request.userIdentity.identityEnvironment, + firstLevelHash, request.userIdentity.privacyBits, + request.userIdentity.establishedAt, request.userIdentity.refreshedAt + ); if (request.shouldCheckOptOut() && getGlobalOptOutResult(firstLevelHashIdentity, false).isOptedOut()) { return IdentityTokens.LogoutToken; @@ -113,8 +118,8 @@ public IdentityTokens generateIdentity(IdentityRequest request, Duration refresh @Override public RefreshResponse refreshIdentity(RefreshToken token, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) { this.validateTokenDurations(refreshIdentityAfter, refreshExpiresAfter, identityExpiresAfter); - // should not be possible as different scopes should be using different keys, but just in case - if (token.userIdentity.identityScope != this.identityScope) { + // should not be possible as different scopes/environments should be using different keys, but just in case + if (token.userIdentity.identityScope != this.identityScope || token.userIdentity.identityEnvironment != this.identityEnvironment) { return RefreshResponse.Invalid; } @@ -174,7 +179,7 @@ public List getModifiedBuckets(Instant sinceTimestamp) { private ISaltProvider.ISaltSnapshot getSaltProviderSnapshot(Instant asOf) { ISaltProvider.ISaltSnapshot snapshot = this.saltProvider.getSnapshot(asOf); - if(snapshot.getExpires().isBefore(Instant.now())) { + if (snapshot.getExpires().isBefore(Instant.now())) { saltRetrievalResponseHandler.handle(true); } else { saltRetrievalResponseHandler.handle(false); @@ -205,19 +210,13 @@ public boolean advertisingTokenMatches(String advertisingToken, UserIdentity use return Arrays.equals(mappedIdentity.advertisingId, token.userIdentity.id); } - @Override - public Instant getLatestOptoutEntry(UserIdentity userIdentity, Instant asOf) { - final UserIdentity firstLevelHashIdentity = getFirstLevelHashIdentity(userIdentity, asOf); - return this.optOutStore.getLatestEntry(firstLevelHashIdentity); - } - private UserIdentity getFirstLevelHashIdentity(UserIdentity userIdentity, Instant asOf) { - return getFirstLevelHashIdentity(userIdentity.identityScope, userIdentity.identityType, userIdentity.id, asOf); + return getFirstLevelHashIdentity(userIdentity.identityScope, userIdentity.identityType, userIdentity.identityEnvironment, userIdentity.id, asOf); } - private UserIdentity getFirstLevelHashIdentity(IdentityScope identityScope, IdentityType identityType, byte[] identityHash, Instant asOf) { + private UserIdentity getFirstLevelHashIdentity(IdentityScope identityScope, IdentityType identityType, IdentityEnvironment identityEnvironment, byte[] identityHash, Instant asOf) { final byte[] firstLevelHash = getFirstLevelHash(identityHash, asOf); - return new UserIdentity(identityScope, identityType, firstLevelHash, 0, null, null); + return new UserIdentity(identityScope, identityType, identityEnvironment, firstLevelHash, 0, null, null); } private byte[] getFirstLevelHash(byte[] identityHash, Instant asOf) { @@ -226,7 +225,7 @@ private byte[] getFirstLevelHash(byte[] identityHash, Instant asOf) { private MappedIdentity getMappedIdentity(UserIdentity firstLevelHashIdentity, Instant asOf) { final SaltEntry rotatingSalt = getSaltProviderSnapshot(asOf).getRotatingSalt(firstLevelHashIdentity.id); - final byte[] advertisingId = getAdvertisingId(firstLevelHashIdentity, rotatingSalt.currentSalt()); + final byte[] advertisingId = getAdvertisingId(firstLevelHashIdentity, rotatingSalt.currentSalt(), rotatingSalt.currentKey()); final byte[] previousAdvertisingId = getPreviousAdvertisingId(firstLevelHashIdentity, rotatingSalt, asOf); final long refreshFrom = getRefreshFrom(rotatingSalt, asOf); @@ -234,22 +233,37 @@ private MappedIdentity getMappedIdentity(UserIdentity firstLevelHashIdentity, In advertisingId, rotatingSalt.hashedId(), previousAdvertisingId, - refreshFrom); + refreshFrom + ); } - private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt) { - return rawUidV3Enabled - ? TokenUtils.getAdvertisingIdV3(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.id, salt) - : TokenUtils.getAdvertisingIdV2(firstLevelHashIdentity.id, salt); + private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt, SaltEntry.KeyMaterial key) { + if (salt != null) { + return rawUidV3Enabled + ? TokenUtils.getAdvertisingIdV3(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.id, salt) + : TokenUtils.getAdvertisingIdV2(firstLevelHashIdentity.id, salt); + } else { + try { + return TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.identityEnvironment, firstLevelHashIdentity.id, key); + } catch (Exception e) { + LOGGER.error("Exception when generating V4 advertising ID", e); + return null; + } + } } private byte[] getPreviousAdvertisingId(UserIdentity firstLevelHashIdentity, SaltEntry rotatingSalt, Instant asOf) { long age = asOf.toEpochMilli() - rotatingSalt.lastUpdated(); if (age / DAY_IN_MS < 90) { - if (rotatingSalt.previousSalt() == null || rotatingSalt.previousSalt().isBlank()) { + boolean missingSalt = rotatingSalt.previousSalt() == null || rotatingSalt.previousSalt().isBlank(); + boolean missingKey = rotatingSalt.previousKey() == null + || rotatingSalt.previousKey().key() == null || rotatingSalt.previousKey().key().isBlank() + || rotatingSalt.previousKey().salt() == null || rotatingSalt.previousKey().salt().isBlank(); + + if (missingSalt && missingKey) { return null; } - return getAdvertisingId(firstLevelHashIdentity, rotatingSalt.previousSalt()); + return getAdvertisingId(firstLevelHashIdentity, rotatingSalt.previousSalt(), rotatingSalt.previousKey()); } return null; } @@ -266,8 +280,10 @@ private IdentityTokens generateIdentity(PublisherIdentity publisherIdentity, Use final Instant nowUtc = EncodingUtils.NowUTCMillis(this.clock); final MappedIdentity mappedIdentity = getMappedIdentity(firstLevelHashIdentity, nowUtc); - final UserIdentity advertisingIdentity = new UserIdentity(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, - mappedIdentity.advertisingId, firstLevelHashIdentity.privacyBits, firstLevelHashIdentity.establishedAt, nowUtc); + final UserIdentity advertisingIdentity = new UserIdentity( + firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.identityEnvironment, + mappedIdentity.advertisingId, firstLevelHashIdentity.privacyBits, firstLevelHashIdentity.establishedAt, nowUtc + ); return this.encoder.encode( this.createAdvertisingToken(publisherIdentity, advertisingIdentity, nowUtc, identityExpiresAfter), @@ -284,19 +300,20 @@ private RefreshToken createRefreshToken(PublisherIdentity publisherIdentity, Use now.plusMillis(refreshExpiresAfter.toMillis()), this.operatorIdentity, publisherIdentity, - userIdentity); + userIdentity + ); } private AdvertisingToken createAdvertisingToken(PublisherIdentity publisherIdentity, UserIdentity userIdentity, Instant now, Duration identityExpiresAfter) { return new AdvertisingToken(TokenVersion.V4, now, now.plusMillis(identityExpiresAfter.toMillis()), this.operatorIdentity, publisherIdentity, userIdentity); } - static protected class GlobalOptoutResult { + protected static class GlobalOptoutResult { private final boolean isOptedOut; - //can be null if isOptedOut is false! + // can be null if isOptedOut is false! private final Instant time; - //providedTime can be null if isOptedOut is false! + // providedTime can be null if isOptedOut is false! GlobalOptoutResult(Instant providedTime) { isOptedOut = providedTime != null; diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java index 986ea9a28..b59f0ecad 100644 --- a/src/main/java/com/uid2/operator/service/V4TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -4,13 +4,10 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import io.vertx.core.buffer.Buffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Arrays; public final class V4TokenUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(V4TokenUtils.class); private static final int IV_LENGTH = 12; private V4TokenUtils() { @@ -61,7 +58,6 @@ public static byte generateChecksum(byte[] data) { for (byte b : data) { checksum ^= b; } - LOGGER.debug("Checksum: 0x{}", String.format("%02X", checksum)); return checksum; } diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 19c53343f..6afa43ba4 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -43,7 +43,6 @@ import io.vertx.core.Handler; import io.vertx.core.Promise; import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.DecodeException; @@ -78,17 +77,18 @@ import static com.uid2.operator.vertx.Endpoints.*; public class UIDOperatorVerticle extends AbstractVerticle { - private static final Logger LOGGER = LoggerFactory.getLogger(UIDOperatorVerticle.class); public static final long MAX_REQUEST_BODY_SIZE = 1 << 20; // 1MB /** * There is currently an issue with v2 tokens (and possibly also other ad token versions) where the token lifetime * is slightly longer than it should be. When validating token lifetimes, we add a small buffer to account for this. */ public static final Duration TOKEN_LIFETIME_TOLERANCE = Duration.ofSeconds(10); - private static final long SECOND_IN_MILLIS = 1000; + private static final Logger LOGGER = LoggerFactory.getLogger(UIDOperatorVerticle.class); // Use a formatter that always prints three-digit millisecond precision (e.g. 2024-07-02T14:15:16.000) private static final DateTimeFormatter API_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").withZone(ZoneOffset.UTC); + private static final ObjectMapper OBJECT_MAPPER = Mapper.getApiInstance(); + private static final long SECOND_IN_MILLIS = 1000; private static final String REQUEST = "request"; private final HealthComponent healthComponent = HealthManager.instance.registerComponent("http-server"); @@ -107,6 +107,7 @@ public class UIDOperatorVerticle extends AbstractVerticle { private final boolean disableOptoutToken; private final UidInstanceIdProvider uidInstanceIdProvider; protected IUIDOperatorService idService; + private final Map _identityMapMetricSummaries = new HashMap<>(); private final Map, DistributionSummary> _refreshDurationMetricSummaries = new HashMap<>(); private final Map, Counter> _advertisingTokenExpiryStatus = new HashMap<>(); @@ -117,6 +118,7 @@ public class UIDOperatorVerticle extends AbstractVerticle { private final Map optOutStatusCounters = new HashMap<>(); private final IdentityScope identityScope; + private final IdentityEnvironment identityEnvironment; private final V2PayloadHandler encryptedPayloadHandler; private final boolean phoneSupport; private final int tcfVendorId; @@ -125,8 +127,8 @@ public class UIDOperatorVerticle extends AbstractVerticle { private final SecureLinkValidatorService secureLinkValidatorService; private final boolean cstgDoDomainNameCheck; private final boolean clientSideTokenGenerateLogInvalidHttpOrigin; - public final static int MASTER_KEYSET_ID_FOR_SDKS = 9999999; //this is because SDKs have an issue where they assume keyset ids are always positive; that will be fixed. - public final static long OPT_OUT_CHECK_CUTOFF_DATE = Instant.parse("2023-09-01T00:00:00.00Z").getEpochSecond(); + public static final int MASTER_KEYSET_ID_FOR_SDKS = 9999999; //this is because SDKs have an issue where they assume keyset ids are always positive; that will be fixed. + public static final long OPT_OUT_CHECK_CUTOFF_DATE = Instant.parse("2023-09-01T00:00:00.00Z").getEpochSecond(); private final Handler saltRetrievalResponseHandler; private final int allowClockSkewSeconds; protected Map> siteIdToInvalidOriginsAndAppNames = new HashMap<>(); @@ -136,19 +138,16 @@ public class UIDOperatorVerticle extends AbstractVerticle { private final int optOutStatusMaxRequestSize; private final boolean optOutStatusApiEnabled; - private final static ObjectMapper OBJECT_MAPPER = Mapper.getApiInstance(); - //"Android" is from https://github.com/IABTechLab/uid2-android-sdk/blob/ff93ebf597f5de7d440a84f7015a334ba4138ede/sdk/src/main/java/com/uid2/UID2Client.kt#L46 //"ios"/"tvos" is from https://github.com/IABTechLab/uid2-ios-sdk/blob/91c290d29a7093cfc209eca493d1fee80c17e16a/Sources/UID2/UID2Client.swift#L36-L38 - private final static List SUPPORTED_IN_APP = Arrays.asList("Android", "ios", "tvos"); + private static final List SUPPORTED_IN_APP = Arrays.asList("Android", "ios", "tvos"); + public static final String ORIGIN_HEADER = "Origin"; private static final String ERROR_INVALID_INPUT_WITH_PHONE_SUPPORT = "Required Parameter Missing: exactly one of [email, email_hash, phone, phone_hash] must be specified"; private static final String ERROR_INVALID_INPUT_EMAIL_MISSING = "Required Parameter Missing: exactly one of email or email_hash must be specified"; private static final String ERROR_INVALID_MIXED_INPUT_WITH_PHONE_SUPPORT = "Required Parameter Missing: one or more of [email, email_hash, phone, phone_hash] must be specified"; private static final String ERROR_INVALID_MIXED_INPUT_EMAIL_MISSING = "Required Parameter Missing: one or more of [email, email_hash] must be specified"; - private static final String ERROR_INVALID_INPUT_EMAIL_TWICE = "Only one of email or email_hash can be specified"; private static final String RC_CONFIG_KEY = "remote-config"; - public final static String ORIGIN_HEADER = "Origin"; public UIDOperatorVerticle(IConfigStore configStore, JsonObject config, @@ -164,6 +163,10 @@ public UIDOperatorVerticle(IConfigStore configStore, SecureLinkValidatorService secureLinkValidatorService, Handler saltRetrievalResponseHandler, UidInstanceIdProvider uidInstanceIdProvider) { + this.identityScope = IdentityScope.fromString(config.getString("identity_scope", "uid2")); + + + this.keyManager = keyManager; this.secureLinkValidatorService = secureLinkValidatorService; try { @@ -175,13 +178,13 @@ public UIDOperatorVerticle(IConfigStore configStore, this.clientSideTokenGenerate = clientSideTokenGenerate; this.healthComponent.setHealthStatus(false, "not started"); this.auth = new AuthMiddleware(clientKeyProvider); - this.encoder = new EncryptedTokenEncoder(keyManager); this.siteProvider = siteProvider; this.clientSideKeypairProvider = clientSideKeypairProvider; this.saltProvider = saltProvider; this.optOutStore = optOutStore; this.clock = clock; - this.identityScope = IdentityScope.fromString(config.getString("identity_scope", "uid2")); + this.identityEnvironment = IdentityEnvironment.fromString(config.getString("identity_environment", "test")); + this.encoder = new EncryptedTokenEncoder(keyManager, identityEnvironment); this.encryptedPayloadHandler = new V2PayloadHandler(keyManager, config.getBoolean("enable_v2_encryption", true), this.identityScope, siteProvider); this.phoneSupport = config.getBoolean("enable_phone_support", true); this.tcfVendorId = config.getInteger("tcf_vendor_id", 21); @@ -208,6 +211,7 @@ public void start(Promise startPromise) throws Exception { this.encoder, this.clock, this.identityScope, + this.identityEnvironment, this.saltRetrievalResponseHandler, this.identityV3Enabled, this.uidInstanceIdProvider @@ -267,7 +271,6 @@ private Router createRoutesSetup() throws IOException { } private void setUpEncryptedRoutes(Router mainRouter, BodyHandler bodyHandler) { - mainRouter.post(V2_TOKEN_GENERATE.toString()).handler(bodyHandler).handler(auth.handleV1( rc -> encryptedPayloadHandler.handleTokenGenerate(rc, this::handleTokenGenerateV2), Role.GENERATOR)); mainRouter.post(V2_TOKEN_REFRESH.toString()).handler(bodyHandler).handler(auth.handleWithOptionalAuth( @@ -297,7 +300,6 @@ private void setUpEncryptedRoutes(Router mainRouter, BodyHandler bodyHandler) { mainRouter.post(V3_IDENTITY_MAP.toString()).handler(bodyHandler).handler(auth.handleV1( rc -> encryptedPayloadHandler.handle(rc, this::handleIdentityMapV3), Role.MAPPER)); - } private void handleClientSideTokenGenerate(RoutingContext rc) { @@ -372,7 +374,7 @@ private void handleClientSideTokenGenerateImpl(RoutingContext rc) throws NoSuchA } rc.put(com.uid2.shared.Const.RoutingContextData.SiteId, clientSideKeypair.getSiteId()); - if(clientSideKeypair.isDisabled()) { + if (clientSideKeypair.isDisabled()) { SendClientErrorResponseAndRecordStats(ResponseStatus.Unauthorized, 401, rc, "Unauthorized", clientSideKeypair.getSiteId(), TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, TokenResponseStatsCollector.ResponseStatus.Unauthorized, siteProvider, platformType); return; @@ -448,7 +450,6 @@ private void handleClientSideTokenGenerateImpl(RoutingContext rc) throws NoSuchA final String phoneHash = requestPayload.getString("phone_hash"); final InputUtil.InputVal input; - if (phoneHash != null && !phoneSupport) { SendClientErrorResponseAndRecordStats(ResponseStatus.ClientError, 400, rc, "phone support not enabled", clientSideKeypair.getSiteId(), TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, TokenResponseStatsCollector.ResponseStatus.BadPayload, siteProvider, platformType); return; @@ -458,15 +459,12 @@ private void handleClientSideTokenGenerateImpl(RoutingContext rc) throws NoSuchA if (emailHash == null && phoneHash == null) { SendClientErrorResponseAndRecordStats(ResponseStatus.ClientError, 400, rc, errString, clientSideKeypair.getSiteId(), TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, TokenResponseStatsCollector.ResponseStatus.MissingParams, siteProvider, platformType); return; - } - else if (emailHash != null && phoneHash != null) { + } else if (emailHash != null && phoneHash != null) { SendClientErrorResponseAndRecordStats(ResponseStatus.ClientError, 400, rc, errString, clientSideKeypair.getSiteId(), TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, TokenResponseStatsCollector.ResponseStatus.BadPayload, siteProvider, platformType); return; - } - else if(emailHash != null) { + } else if (emailHash != null) { input = InputUtil.normalizeEmailHash(emailHash); - } - else { + } else { input = InputUtil.normalizePhoneHash(phoneHash); } @@ -483,12 +481,12 @@ else if(emailHash != null) { identityTokens = this.idService.generateIdentity( new IdentityRequest( new PublisherIdentity(clientSideKeypair.getSiteId(), 0, 0), - input.toUserIdentity(this.identityScope, privacyBits.getAsInt(), Instant.now()), + input.toUserIdentity(this.identityScope, this.identityEnvironment, privacyBits.getAsInt(), Instant.now()), OptoutCheckPolicy.RespectOptOut), refreshIdentityAfter, refreshExpiresAfter, identityExpiresAfter); - } catch (KeyManager.NoActiveKeyException e){ + } catch (KeyManager.NoActiveKeyException e) { SendServerErrorResponseAndRecordStats(rc, "No active encryption key available", clientSideKeypair.getSiteId(), TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, TokenResponseStatsCollector.ResponseStatus.NoActiveKey, siteProvider, e, platformType); return; } @@ -498,8 +496,7 @@ else if(emailHash != null) { if (identityTokens.isEmptyToken()) { response = ResponseUtil.SuccessNoBodyV2(ResponseStatus.OptOut); responseStatus = TokenResponseStatsCollector.ResponseStatus.OptOut; - } - else { //user not opted out and already generated valid identity token + } else { //user not opted out and already generated valid identity token response = ResponseUtil.SuccessV2(toTokenResponseJson(identityTokens)); } //if returning an optout token or a successful identity token created originally @@ -862,7 +859,7 @@ private void handleTokenValidateV2(RoutingContext rc) { final Instant now = Instant.now(); final String token = req.getString("token"); - if (this.idService.advertisingTokenMatches(token, input.toUserIdentity(this.identityScope, 0, now), now)) { + if (this.idService.advertisingTokenMatches(token, input.toUserIdentity(this.identityScope, this.identityEnvironment, 0, now), now)) { ResponseUtil.SuccessV2(rc, Boolean.TRUE); } else { ResponseUtil.SuccessV2(rc, Boolean.FALSE); @@ -927,7 +924,7 @@ private void handleTokenGenerateV2(RoutingContext rc) { final IdentityTokens t = this.idService.generateIdentity( new IdentityRequest( new PublisherIdentity(siteId, 0, 0), - input.toUserIdentity(this.identityScope, 1, Instant.now()), + input.toUserIdentity(this.identityScope, this.identityEnvironment, 1, Instant.now()), OptoutCheckPolicy.respectOptOut()), refreshIdentityAfter, refreshExpiresAfter, @@ -946,7 +943,7 @@ private void handleTokenGenerateV2(RoutingContext rc) { final IdentityTokens optOutTokens = this.idService.generateIdentity( new IdentityRequest( new PublisherIdentity(siteId, 0, 0), - optOutTokenInput.toUserIdentity(this.identityScope, pb.getAsInt(), Instant.now()), + optOutTokenInput.toUserIdentity(this.identityScope, this.identityEnvironment, pb.getAsInt(), Instant.now()), OptoutCheckPolicy.DoNotRespect), refreshIdentityAfter, refreshExpiresAfter, @@ -980,7 +977,7 @@ private Future handleLogoutAsyncV2(RoutingContext rc) { final Instant now = Instant.now(); Promise promise = Promise.promise(); - this.idService.invalidateTokensAsync(input.toUserIdentity(this.identityScope, 0, now), now, uidTraceId, ar -> { + this.idService.invalidateTokensAsync(input.toUserIdentity(this.identityScope, this.identityEnvironment, 0, now), now, uidTraceId, ar -> { if (ar.succeeded()) { JsonObject body = new JsonObject(); body.put("optout", "OK"); @@ -1088,7 +1085,7 @@ private JsonObject handleIdentityMapCommon(RoutingContext rc, InputUtil.InputVal if (input != null && input.isValid()) { final MappedIdentity mappedIdentity = idService.mapIdentity( new MapRequest( - input.toUserIdentity(this.identityScope, 0, now), + input.toUserIdentity(this.identityScope, this.identityEnvironment, 0, now), OptoutCheckPolicy.respectOptOut(), now)); @@ -1139,7 +1136,7 @@ private JsonObject processIdentityMapV3Response(RoutingContext rc, Map saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) { - super(optOutStore, saltProvider, encoder, clock, identityScope, saltRetrievalResponseHandler, identityV3Enabled, uidInstanceIdProvider); + + private EncryptedTokenEncoder tokenEncoder; + private UidInstanceIdProvider uidInstanceIdProvider; + private JsonObject uid2Config; + private JsonObject euidConfig; + private ExtendedUIDOperatorService uid2Service; + private ExtendedUIDOperatorService euidService; + private Instant now; + + static class ExtendedUIDOperatorService extends UIDOperatorService { + public ExtendedUIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, IdentityScope identityScope, IdentityEnvironment identityEnvironment, Handler saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) { + super(optOutStore, saltProvider, encoder, clock, identityScope, identityEnvironment, saltRetrievalResponseHandler, identityV3Enabled, uidInstanceIdProvider); } } @@ -88,7 +88,7 @@ void setup() throws Exception { "/com.uid2.core/test/salts/metadata.json"); saltProvider.loadContent(); - tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); setNow(Instant.now()); @@ -106,6 +106,7 @@ void setup() throws Exception { tokenEncoder, this.clock, IdentityScope.UID2, + IdentityEnvironment.Test, this.shutdownHandler::handleSaltRetrievalResponse, uid2Config.getBoolean(IdentityV3Prop), uidInstanceIdProvider @@ -123,6 +124,7 @@ void setup() throws Exception { tokenEncoder, this.clock, IdentityScope.EUID, + IdentityEnvironment.Test, this.shutdownHandler::handleSaltRetrievalResponse, euidConfig.getBoolean(IdentityV3Prop), uidInstanceIdProvider @@ -152,6 +154,7 @@ private RotatingSaltProvider.SaltSnapshot setUpMockSalts() { tokenEncoder, this.clock, IdentityScope.UID2, + IdentityEnvironment.Test, this.shutdownHandler::handleSaltRetrievalResponse, uid2Config.getBoolean(IdentityV3Prop), uidInstanceIdProvider @@ -160,10 +163,11 @@ private RotatingSaltProvider.SaltSnapshot setUpMockSalts() { return saltSnapshot; } - private UserIdentity createUserIdentity(String rawIdentityHash, IdentityScope scope, IdentityType type) { + private UserIdentity createUserIdentity(String rawIdentityHash, IdentityScope scope, IdentityType type, IdentityEnvironment environment) { return new UserIdentity( scope, type, + environment, rawIdentityHash.getBytes(StandardCharsets.UTF_8), 0, this.now.minusSeconds(234), @@ -183,11 +187,11 @@ private void assertIdentityScopeIdentityTypeAndEstablishedAt(UserIdentity expcte } @ParameterizedTest - @CsvSource({"123, V4","127, V4","128, V4"}) - public void testGenerateAndRefresh(int siteId, TokenVersion tokenVersion) { + @CsvSource({"123, V4", "127, V4", "128, V4"}) + void testGenerateAndRefresh(int siteId, TokenVersion tokenVersion) { final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(siteId, 124, 125), - createUserIdentity("test-email-hash", IdentityScope.UID2, IdentityType.Email), + createUserIdentity("test-email-hash", IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.Test), OptoutCheckPolicy.DoNotRespect ); final IdentityTokens tokens = uid2Service.generateIdentity( @@ -200,7 +204,8 @@ public void testGenerateAndRefresh(int siteId, TokenVersion tokenVersion) { assertNotNull(tokens); UIDOperatorVerticleTest.validateAdvertisingToken(tokens.getAdvertisingToken(), tokenVersion, IdentityScope.UID2, IdentityType.Email); - AdvertisingToken advertisingToken = tokenEncoder.decodeAdvertisingToken(tokens.getAdvertisingToken());assertEquals(this.now.plusSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS), advertisingToken.expiresAt); + AdvertisingToken advertisingToken = tokenEncoder.decodeAdvertisingToken(tokens.getAdvertisingToken()); + assertEquals(this.now.plusSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS), advertisingToken.expiresAt); assertEquals(identityRequest.publisherIdentity.siteId, advertisingToken.publisherIdentity.siteId); assertIdentityScopeIdentityTypeAndEstablishedAt(identityRequest.userIdentity, advertisingToken.userIdentity); @@ -240,12 +245,12 @@ public void testGenerateAndRefresh(int siteId, TokenVersion tokenVersion) { } @Test - public void testTestOptOutKey_DoNotRespectOptout() { + void testTestOptOutKey_DoNotRespectOptout() { final InputUtil.InputVal inputVal = InputUtil.normalizeEmail(IdentityConst.OptOutIdentityForEmail); final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(IdentityScope.UID2, 0, this.now), + inputVal.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.DoNotRespect ); final IdentityTokens tokens = uid2Service.generateIdentity( @@ -267,12 +272,12 @@ public void testTestOptOutKey_DoNotRespectOptout() { } @Test - public void testTestOptOutKey_RespectOptout() { + void testTestOptOutKey_RespectOptout() { final InputUtil.InputVal inputVal = InputUtil.normalizeEmail(IdentityConst.OptOutIdentityForEmail); final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(IdentityScope.UID2, 0, this.now), + inputVal.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut ); final IdentityTokens tokens = uid2Service.generateIdentity( @@ -286,13 +291,13 @@ public void testTestOptOutKey_RespectOptout() { } @Test - public void testTestOptOutKeyIdentityScopeMismatch() { + void testTestOptOutKeyIdentityScopeMismatch() { final String email = "optout@example.com"; final InputUtil.InputVal inputVal = InputUtil.normalizeEmail(email); final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(IdentityScope.EUID, 0, this.now), + inputVal.toUserIdentity(IdentityScope.EUID, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.DoNotRespect ); final IdentityTokens tokens = euidService.generateIdentity( @@ -315,12 +320,14 @@ public void testTestOptOutKeyIdentityScopeMismatch() { } @ParameterizedTest - @CsvSource({"Email,test@example.com,UID2", - "Email,test@example.com,EUID", - "Phone,+01010101010,UID2", - "Phone,+01010101010,EUID"}) - public void testGenerateTokenForOptOutUser(IdentityType type, String identity, IdentityScope scope) { - final UserIdentity userIdentity = createUserIdentity(identity, scope, type); + @CsvSource({ + "Email,test@example.com,UID2,Test", + "Email,test@example.com,EUID,Test", + "Phone,+01010101010,UID2,Test", + "Phone,+01010101010,EUID,Test" + }) + void testGenerateTokenForOptOutUser(IdentityType type, String identity, IdentityScope scope, IdentityEnvironment environment) { + final UserIdentity userIdentity = createUserIdentity(identity, scope, type, environment); final IdentityRequest identityRequestForceGenerate = new IdentityRequest( new PublisherIdentity(123, 124, 125), @@ -381,12 +388,12 @@ public void testGenerateTokenForOptOutUser(IdentityType type, String identity, I } @ParameterizedTest - @CsvSource({"Email,test@example.com,UID2", - "Email,test@example.com,EUID", - "Phone,+01010101010,UID2", - "Phone,+01010101010,EUID"}) - public void testIdentityMapForOptOutUser(IdentityType type, String identity, IdentityScope scope) { - final UserIdentity userIdentity = createUserIdentity(identity, scope, type); + @CsvSource({"Email,test@example.com,UID2,Test", + "Email,test@example.com,EUID,Test", + "Phone,+01010101010,UID2,Test", + "Phone,+01010101010,EUID,Test"}) + void testIdentityMapForOptOutUser(IdentityType type, String identity, IdentityScope scope, IdentityEnvironment environment) { + final UserIdentity userIdentity = createUserIdentity(identity, scope, type, environment); final Instant now = Instant.now(); final MapRequest mapRequestForceMap = new MapRequest( @@ -471,7 +478,7 @@ void testSpecialIdentityOptOutTokenGenerate(TestIdentityInputType type, String i final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut ); @@ -511,7 +518,7 @@ void testSpecialIdentityOptOutIdentityMap(TestIdentityInputType type, String id, InputUtil.InputVal inputVal = generateInputVal(type, id); final MapRequest mapRequestRespectOptOut = new MapRequest( - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); @@ -545,7 +552,7 @@ void testSpecialIdentityOptOutTokenRefresh(TestIdentityInputType type, String id final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.DoNotRespect ); @@ -596,7 +603,7 @@ void testSpecialIdentityRefreshOptOutGenerate(TestIdentityInputType type, String final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut ); @@ -649,7 +656,7 @@ void testSpecialIdentityRefreshOptOutIdentityMap(TestIdentityInputType type, Str InputUtil.InputVal inputVal = generateInputVal(type, id); final MapRequest mapRequestRespectOptOut = new MapRequest( - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); @@ -683,7 +690,7 @@ void testSpecialIdentityValidateGenerate(TestIdentityInputType type, String id, final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut ); @@ -728,7 +735,7 @@ void testSpecialIdentityValidateIdentityMap(TestIdentityInputType type, String i InputUtil.InputVal inputVal = generateInputVal(type, id); final MapRequest mapRequestRespectOptOut = new MapRequest( - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); @@ -759,7 +766,7 @@ void testNormalIdentityOptIn(TestIdentityInputType type, String id, IdentityScop InputUtil.InputVal inputVal = generateInputVal(type, id); final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.DoNotRespect ); IdentityTokens tokens; @@ -794,12 +801,14 @@ void testNormalIdentityOptIn(TestIdentityInputType type, String id, IdentityScop } @ParameterizedTest - @CsvSource({"Email,blah@unifiedid.com,UID2", + @CsvSource({ + "Email,blah@unifiedid.com,UID2", "EmailHash,blah@unifiedid.com,UID2", "Phone,+61401234567,EUID", "PhoneHash,+61401234567,EUID", "Email,blah@unifiedid.com,EUID", - "EmailHash,blah@unifiedid.com,EUID"}) + "EmailHash,blah@unifiedid.com,EUID" + }) void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String id, IdentityScope scope) throws Exception { RotatingSaltProvider saltProvider = new RotatingSaltProvider( new EmbeddedResourceStorage(Main.class), @@ -812,6 +821,7 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String tokenEncoder, this.clock, IdentityScope.UID2, + IdentityEnvironment.Test, this.shutdownHandler::handleSaltRetrievalResponse, uid2Config.getBoolean(IdentityV3Prop), uidInstanceIdProvider @@ -823,6 +833,7 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String tokenEncoder, this.clock, IdentityScope.EUID, + IdentityEnvironment.Test, this.shutdownHandler::handleSaltRetrievalResponse, euidConfig.getBoolean(IdentityV3Prop), uidInstanceIdProvider @@ -834,21 +845,20 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String final IdentityRequest identityRequest = new IdentityRequest( new PublisherIdentity(123, 124, 125), - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut); IdentityTokens tokens; AdvertisingToken advertisingToken; reset(shutdownHandler); - if(scope == IdentityScope.EUID) { + if (scope == IdentityScope.EUID) { tokens = euidService.generateIdentity( identityRequest, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); advertisingToken = validateAndGetToken(tokenEncoder, tokens.getAdvertisingToken(), IdentityScope.EUID, identityRequest.userIdentity.identityType, identityRequest.publisherIdentity.siteId); - } - else { + } else { tokens = uid2Service.generateIdentity( identityRequest, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), @@ -876,15 +886,15 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String assertNotEquals(RefreshResponse.Optout, refreshResponse); final MapRequest mapRequest = new MapRequest( - inputVal.toUserIdentity(scope, 0, this.now), + inputVal.toUserIdentity(scope, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, - now); + now + ); final MappedIdentity mappedIdentity; reset(shutdownHandler); - if(scope == IdentityScope.EUID) { + if (scope == IdentityScope.EUID) { mappedIdentity = euidService.mapIdentity(mapRequest); - } - else { + } else { mappedIdentity = uid2Service.mapIdentity(mapRequest); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(true); @@ -906,7 +916,7 @@ void testMappedIdentityWithPreviousSaltReturnsPreviousUid() { var email = "test@uid.com"; InputUtil.InputVal emailInput = generateInputVal(TestIdentityInputType.Email, email); - MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); + MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); @@ -929,7 +939,7 @@ void testMappedIdentityWithOutdatedPreviousSaltReturnsNoPreviousUid(long extraMs var email = "test@uid.com"; InputUtil.InputVal emailInput = generateInputVal(TestIdentityInputType.Email, email); - MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); + MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); var expectedCurrentUID = UIDOperatorVerticleTest.getRawUid(IdentityType.Email, email, FIRST_LEVEL_SALT, salt.currentSalt(), IdentityScope.UID2, uid2Config.getBoolean(IdentityV3Prop)); @@ -949,7 +959,7 @@ void testMappedIdentityWithNoPreviousSaltReturnsNoPreviousUid() { var email = "test@uid.com"; InputUtil.InputVal emailInput = generateInputVal(TestIdentityInputType.Email, email); - MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); + MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); @@ -971,7 +981,7 @@ void testMappedIdentityWithValidRefreshFrom(int refreshFromDays) { var email = "test@uid.com"; InputUtil.InputVal emailInput = generateInputVal(TestIdentityInputType.Email, email); - MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); + MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); @@ -990,7 +1000,7 @@ void testMappedIdentityWithOutdatedRefreshFrom() { var email = "test@uid.com"; InputUtil.InputVal emailInput = generateInputVal(TestIdentityInputType.Email, email); - MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); + MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 97c6890a2..a38f6ca12 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -752,7 +752,7 @@ void verticleDeployed(Vertx vertx, VertxTestContext testContext) { @ParameterizedTest @CsvSource({"v2, text/plain", - "v2, application/octet-stream"}) + "v2, application/octet-stream"}) void keyLatestNoAcl(String apiVersion, String contentType, Vertx vertx, VertxTestContext testContext) { fakeAuth(5, Role.ID_READER); Keyset[] keysets = { @@ -939,7 +939,7 @@ RefreshToken decodeRefreshToken(EncryptedTokenEncoder encoder, String refreshTok @ParameterizedTest @CsvSource({"v2, text/plain", - "v2, application/octet-stream"}) + "v2, application/octet-stream"}) void identityMapNewClientNoPolicySpecified(String apiVersion, String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, newClientCreationDateTime, Role.MAPPER); @@ -995,7 +995,7 @@ void fallbackToBase64DecodingIfBinaryEnvelopeDecodeFails(Vertx vertx, VertxTestC testContext.completeNow(); - // Set request content type header to application/octet-stream, but force a base64 encoded request envelope + // Set request content type header to application/octet-stream, but force a base64 encoded request envelope }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), "application/octet-stream"), true); } @@ -1146,16 +1146,16 @@ void v3IdentityMapMixedInputSuccess(String contentType, Vertx vertx, VertxTestCo assertEquals(2, mappedEmails.size()); var mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email,"test1@uid2.com", firstLevelSalt, salt.previousSalt())), - "r", refreshFrom.getEpochSecond() + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email,"test1@uid2.com", firstLevelSalt, salt.previousSalt())), + "r", refreshFrom.getEpochSecond() ); assertEquals(mappedEmailExpected1, mappedEmails.getJsonObject(0)); var mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email,"test2@uid2.com", firstLevelSalt, salt.previousSalt())), - "r", refreshFrom.getEpochSecond() + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email,"test2@uid2.com", firstLevelSalt, salt.previousSalt())), + "r", refreshFrom.getEpochSecond() ); assertEquals(mappedEmailExpected2, mappedEmails.getJsonObject(1)); @@ -1166,9 +1166,9 @@ void v3IdentityMapMixedInputSuccess(String contentType, Vertx vertx, VertxTestCo assertEquals(1, mappedPhoneHash.size()); var mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt.previousSalt())), - "r", refreshFrom.getEpochSecond() + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt.previousSalt())), + "r", refreshFrom.getEpochSecond() ); assertEquals(mappedPhoneHashExpected, mappedPhoneHash.getJsonObject(0)); @@ -1262,8 +1262,8 @@ void v3IdentityMapMissingValidInputKeys(String inputPayload, Vertx vertx, VertxT @ParameterizedTest @ValueSource(strings = {"{\"invalid_key\": []}", - "{\"email\": [ null ]}", - "{\"email\": [ \"some_email\", null ]}" + "{\"email\": [ null ]}", + "{\"email\": [ \"some_email\", null ]}" }) void v3IdentityMapIncorrectInputFormats(String inputPayload, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; @@ -1300,9 +1300,9 @@ void v3IdentityMapNoPreviousAdvertisingId(String previousSalt, Vertx vertx, Vert var mappedEmails = body.getJsonArray("email"); var expectedMappedEmails = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentSalt())), - "p", null, - "r", refreshFrom.getEpochSecond() + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentSalt())), + "p", null, + "r", refreshFrom.getEpochSecond() ); assertEquals(expectedMappedEmails, mappedEmails.getJsonObject(0)); @@ -1311,7 +1311,7 @@ void v3IdentityMapNoPreviousAdvertisingId(String previousSalt, Vertx vertx, Vert } @Test - void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) { + void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); @@ -1332,9 +1332,9 @@ void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) var mappedEmails = body.getJsonArray("email"); var expectedMappedEmails = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentSalt())), - "p", null, - "r", asOf.truncatedTo(DAYS).plus(1, DAYS).getEpochSecond() + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentSalt())), + "p", null, + "r", asOf.truncatedTo(DAYS).plus(1, DAYS).getEpochSecond() ); assertEquals(expectedMappedEmails, mappedEmails.getJsonObject(0)); @@ -1467,10 +1467,12 @@ void tokenGenerateNewClientWrongPolicySpecifiedOlderKeySuccessful(String policyP } @ParameterizedTest // TODO: remove test after optout check phase 3 - @CsvSource({"policy,someoptout@example.com,Email", + @CsvSource({ + "policy,someoptout@example.com,Email", "policy,+01234567890,Phone", "optout_check,someoptout@example.com,Email", - "optout_check,+01234567890,Phone"}) + "optout_check,+01234567890,Phone" + }) void tokenGenerateOptOutToken(String policyParameterKey, String identity, IdentityType identityType, Vertx vertx, VertxTestContext testContext) { ClientKey oldClientKey = new ClientKey( @@ -1508,7 +1510,7 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi decodeV2RefreshToken(json); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, identityType); RefreshToken refreshToken = encoder.decodeRefreshToken(body.getString("decrypted_refresh_token")); @@ -1523,7 +1525,7 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi String advertisingTokenString = body.getString("advertising_token"); final Instant now = Instant.now(); final String token = advertisingTokenString; - final boolean matchedOptedOutIdentity = this.uidOperatorVerticle.getIdService().advertisingTokenMatches(token, optOutTokenInput.toUserIdentity(getIdentityScope(), 0, now), now); + final boolean matchedOptedOutIdentity = this.uidOperatorVerticle.getIdService().advertisingTokenMatches(token, optOutTokenInput.toUserIdentity(getIdentityScope(), IdentityEnvironment.Test, 0, now), now); assertTrue(matchedOptedOutIdentity); assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); assertTrue(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); @@ -1534,8 +1536,7 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi TokenResponseStatsCollector.ResponseStatus.Success, TokenResponseStatsCollector.PlatformType.Other); - sendTokenRefresh("v2", vertx, testContext, body.getString("refresh_token"), body.getString("refresh_response_key"), 200, refreshRespJson -> - { + sendTokenRefresh("v2", vertx, testContext, body.getString("refresh_token"), body.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNull(refreshBody); @@ -1550,10 +1551,12 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi } @ParameterizedTest // TODO: remove test after optout check phase 3 - @CsvSource({"policy,someoptout@example.com,Email", + @CsvSource({ + "policy,someoptout@example.com,Email", "policy,+01234567890,Phone", "optout_check,someoptout@example.com,Email", - "optout_check,+01234567890,Phone"}) + "optout_check,+01234567890,Phone" + }) void tokenGenerateOptOutTokenWithDisableOptoutTokenFF(String policyParameterKey, String identity, IdentityType identityType, Vertx vertx, VertxTestContext testContext) { ClientKey oldClientKey = new ClientKey( @@ -1595,8 +1598,10 @@ void tokenGenerateOptOutTokenWithDisableOptoutTokenFF(String policyParameterKey, } @ParameterizedTest - @CsvSource({"v2, text/plain", - "v2, application/octet-stream"}) + @CsvSource({ + "v2, text/plain", + "v2, application/octet-stream" + }) void tokenGenerateForEmail(String apiVersion, String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; @@ -1614,7 +1619,7 @@ void tokenGenerateForEmail(String apiVersion, String contentType, Vertx vertx, V assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -1635,7 +1640,7 @@ void tokenGenerateForEmail(String apiVersion, String contentType, Vertx vertx, V testContext.completeNow(); }, - Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); + Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); } @ParameterizedTest @@ -1657,7 +1662,7 @@ void tokenGenerateForEmailHash(String apiVersion, Vertx vertx, VertxTestContext assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -1679,8 +1684,10 @@ void tokenGenerateForEmailHash(String apiVersion, Vertx vertx, VertxTestContext } @ParameterizedTest - @CsvSource({"v2, text/plain", - "v2, application/octet-stream"}) + @CsvSource({ + "v2, text/plain", + "v2, application/octet-stream" + }) void tokenGenerateThenRefresh(String apiVersion, String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; @@ -1689,7 +1696,7 @@ void tokenGenerateThenRefresh(String apiVersion, String contentType, Vertx vertx setupKeys(); Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, - HttpHeaders.CONTENT_TYPE.toString(), contentType); + HttpHeaders.CONTENT_TYPE.toString(), contentType); generateTokens(apiVersion, vertx, "email", emailAddress, genRespJson -> { assertEquals("success", genRespJson.getString("status")); @@ -1700,12 +1707,11 @@ void tokenGenerateThenRefresh(String apiVersion, String contentType, Vertx vertx when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - sendTokenRefresh(apiVersion, vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> - { + sendTokenRefresh(apiVersion, vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Email); @@ -1731,7 +1737,7 @@ void tokenGenerateThenRefresh(String apiVersion, String contentType, Vertx vertx TokenResponseStatsCollector.PlatformType.InApp); assertTokenStatusMetrics( clientSiteId, - TokenResponseStatsCollector.Endpoint.RefreshV2, + TokenResponseStatsCollector.Endpoint.RefreshV2, TokenResponseStatsCollector.ResponseStatus.Success, TokenResponseStatsCollector.PlatformType.InApp); @@ -1761,12 +1767,11 @@ void tokenGenerateThenRefreshSaltsExpired(String apiVersion, Vertx vertx, VertxT when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - sendTokenRefresh(apiVersion, vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> - { + sendTokenRefresh(apiVersion, vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Email); @@ -1824,8 +1829,7 @@ void tokenGenerateThenRefreshNoActiveKey(Vertx vertx, VertxTestContext testConte String genRefreshToken = bodyJson.getString("refresh_token"); setupKeys(true); - sendTokenRefresh("v2", vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 500, refreshRespJson -> - { + sendTokenRefresh("v2", vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 500, refreshRespJson -> { assertFalse(refreshRespJson.containsKey("body")); assertEquals("No active encryption key available", refreshRespJson.getString("message")); testContext.completeNow(); @@ -1833,7 +1837,6 @@ void tokenGenerateThenRefreshNoActiveKey(Vertx vertx, VertxTestContext testConte }); } - @ParameterizedTest @ValueSource(strings = {"v2"}) void tokenGenerateThenValidateWithEmail_Match(String apiVersion, Vertx vertx, VertxTestContext testContext) { @@ -1944,7 +1947,7 @@ void tokenGenerateUsingCustomSiteKey(String apiVersion, Vertx vertx, VertxTestCo assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); @@ -1978,7 +1981,7 @@ void tokenGenerateSaltsExpired(String apiVersion, Vertx vertx, VertxTestContext assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -2213,7 +2216,7 @@ void tokenRefreshOptOutBeforeLogin(String apiVersion, Vertx vertx, VertxTestCont @ParameterizedTest @CsvSource({"v2, text/plain", - "v2, application/octet-stream"}) + "v2, application/octet-stream"}) void tokenValidateWithEmail_Mismatch(String apiVersion, String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = ValidateIdentityForEmail; @@ -2231,7 +2234,7 @@ void tokenValidateWithEmail_Mismatch(String apiVersion, String contentType, Vert testContext.completeNow(); }, - Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); + Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); } @ParameterizedTest @@ -2326,7 +2329,6 @@ void identityMapSingleEmailProvided(String apiVersion, Vertx vertx, VertxTestCon setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("email", "test@example.com"); send(apiVersion, vertx, apiVersion + "/identity/map", false, null, req, 400, json -> { @@ -2347,7 +2349,6 @@ void identityMapSingleEmailHashProvided(String apiVersion, Vertx vertx, VertxTes setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("email_hash", "test@example.com"); send(apiVersion, vertx, apiVersion + "/identity/map", false, null, req, 400, json -> { @@ -2368,7 +2369,6 @@ void identityMapSinglePhoneProvided(String apiVersion, Vertx vertx, VertxTestCon setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("phone", "555-555-5555"); send(apiVersion, vertx, apiVersion + "/identity/map", false, null, req, 400, json -> { @@ -2389,7 +2389,6 @@ void identityMapSinglePhoneHashProvided(String apiVersion, Vertx vertx, VertxTes setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("phone_hash", "555-555-5555"); send(apiVersion, vertx, apiVersion + "/identity/map", false, null, req, 400, json -> { @@ -2428,17 +2427,17 @@ void identityMapBatchEmailHashes(String apiVersion, Vertx vertx, VertxTestContex JsonObject req = new JsonObject(); JsonArray hashes = new JsonArray(); req.put("email_hash", hashes); - final String[] email_hashes = { + final String[] emailHashes = { TokenUtils.getIdentityHashString("test1@uid2.com"), TokenUtils.getIdentityHashString("test2@uid2.com"), }; - for (String email_hash : email_hashes) { - hashes.add(email_hash); + for (String emailHash : emailHashes) { + hashes.add(emailHash); } send(apiVersion, vertx, apiVersion + "/identity/map", false, null, req, 200, json -> { - checkIdentityMapResponse(json, email_hashes); + checkIdentityMapResponse(json, emailHashes); testContext.completeNow(); }); } @@ -2604,8 +2603,11 @@ void optOutStatusUnauthorized(String contentType, Vertx vertx, VertxTestContext } @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void LogoutV2(String contentType, Vertx vertx, VertxTestContext testContext) { + @ValueSource(strings = { + "text/plain", + "application/octet-stream" + }) + void logoutV2(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.OPTOUT); setupSalts(); @@ -2629,7 +2631,7 @@ void LogoutV2(String contentType, Vertx vertx, VertxTestContext testContext) { } @Test - void LogoutV2SaltsExpired(Vertx vertx, VertxTestContext testContext) { + void logoutV2SaltsExpired(Vertx vertx, VertxTestContext testContext) { when(saltProviderSnapshot.getExpires()).thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); final int clientSiteId = 201; fakeAuth(clientSiteId, Role.OPTOUT); @@ -2743,7 +2745,7 @@ void tokenGenerateForPhone(String apiVersion, Vertx vertx, VertxTestContext test assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Phone); @@ -2782,7 +2784,7 @@ void tokenGenerateForPhoneHash(String apiVersion, Vertx vertx, VertxTestContext assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Phone); @@ -2821,12 +2823,11 @@ void tokenGenerateThenRefreshForPhone(String apiVersion, Vertx vertx, VertxTestC when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - sendTokenRefresh(apiVersion, vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> - { + sendTokenRefresh(apiVersion, vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Phone); @@ -3297,8 +3298,7 @@ private void setupCstgBackend(String... domainNames) { setupCstgBackend(List.of(domainNames), Collections.emptyList()); } - private void setupCstgBackend(List domainNames, List appNames) - { + private void setupCstgBackend(List domainNames, List appNames) { setupSalts(); setupKeys(); ClientSideKeypair keypair = new ClientSideKeypair(clientSideTokenGenerateSubscriptionId, clientSideTokenGeneratePublicKey, clientSideTokenGeneratePrivateKey, clientSideTokenGenerateSiteId, "", Instant.now(), false, ""); @@ -3562,7 +3562,7 @@ void cstgDomainNameCheckPasses(String httpOrigin, Vertx vertx, VertxTestContext JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct testContext.completeNow(); }); @@ -3589,7 +3589,7 @@ void cstgAppNameCheckPasses(String appName, Vertx vertx, VertxTestContext testCo JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct assertTokenStatusMetrics( clientSideTokenGenerateSiteId, @@ -4114,7 +4114,7 @@ void cstgUserOptsOutAfterTokenGenerate(String id, IdentityType identityType, Ver when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(null); - final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(UserIdentity.class); sendCstg(vertx, @@ -4164,12 +4164,10 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); - if(optOutExpected) - { + if (optOutExpected) { when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); - } - else { //not expectedOptedOut + } else { //not expectedOptedOut when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(null); } @@ -4193,7 +4191,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id assertNotNull(genBody); decodeV2RefreshToken(respJson); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); @@ -4212,12 +4210,11 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id String genRefreshToken = genBody.getString("refresh_token"); //test a subsequent refresh from this cstg call and see if it still works - sendTokenRefresh("v2", vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> - { + sendTokenRefresh("v2", vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); //make sure the new advertising token from refresh looks right AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder2, refreshBody, identityType); @@ -4264,7 +4261,7 @@ void cstgSaltsExpired(String httpOrigin, Vertx vertx, VertxTestContext testConte JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(true); @@ -4579,7 +4576,7 @@ void tokenGenerateRotatingKeysets_GENERATOR(String testRun, Vertx vertx, VertxTe assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); @@ -4707,11 +4704,9 @@ private void setupMockSites(Map sites) { }); } - static Map setupExpectation(boolean includeDomainNames, boolean includeAppNames, int... siteIds) - { + static Map setupExpectation(boolean includeDomainNames, boolean includeAppNames, int... siteIds) { Map expectedSites = new HashMap<>(); - for (int siteId : siteIds) - { + for (int siteId : siteIds) { if (includeDomainNames || includeAppNames) { expectedSites.put(siteId, defaultMockSite(siteId, includeDomainNames, includeAppNames)); } @@ -4719,12 +4714,9 @@ static Map setupExpectation(boolean includeDomainNames, boolean i return expectedSites; } - public void verifyExpectedSiteDetail(Map expectedSites, JsonArray actualResult) { - assertEquals(expectedSites.size(), actualResult.size()); for(int i = 0; i < actualResult.size(); i++) { - JsonObject siteDetail = actualResult.getJsonObject(i); int siteId = siteDetail.getInteger("id"); List actualDomainList = (List) siteDetail.getMap().get("domain_names"); diff --git a/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java b/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java index 10565009b..4a4c9a52a 100644 --- a/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java +++ b/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java @@ -45,13 +45,15 @@ import java.util.List; import java.util.Random; -public class BenchmarkCommon { +public final class BenchmarkCommon { + public static final int IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS = 600; + public static final int REFRESH_TOKEN_EXPIRES_AFTER_SECONDS = 900; + public static final int REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = 300; - final static int IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS = 600; - final static int REFRESH_TOKEN_EXPIRES_AFTER_SECONDS = 900; - final static int REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = 300; + private BenchmarkCommon() { + } - static IUIDOperatorService createUidOperatorService() throws Exception { + public static IUIDOperatorService createUidOperatorService() throws Exception { RotatingKeysetKeyStore keysetKeyStore = new RotatingKeysetKeyStore( new EmbeddedResourceStorage(Main.class), new GlobalScope(new CloudPath("/com.uid2.core/test/keyset_keys/metadata.json"))); @@ -67,7 +69,7 @@ static IUIDOperatorService createUidOperatorService() throws Exception { "/com.uid2.core/test/salts/metadata.json"); saltProvider.loadContent(); - final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); final List optOutPartitionFiles = new ArrayList<>(); final ICloudStorage optOutLocalStorage = make1mOptOutEntryStorage( saltProvider.getSnapshot(Instant.now()).getFirstLevelSalt(), @@ -80,13 +82,14 @@ static IUIDOperatorService createUidOperatorService() throws Exception { tokenEncoder, Clock.systemUTC(), IdentityScope.UID2, + IdentityEnvironment.Test, shutdownHandler::handleSaltRetrievalResponse, false, new UidInstanceIdProvider("test-instance", "id") ); } - static EncryptedTokenEncoder createTokenEncoder() throws Exception { + public static EncryptedTokenEncoder createTokenEncoder() throws Exception { RotatingKeysetKeyStore keysetKeyStore = new RotatingKeysetKeyStore( new EmbeddedResourceStorage(Main.class), new GlobalScope(new CloudPath("/com.uid2.core/test/keyset_keys/metadata.json"))); @@ -97,10 +100,10 @@ static EncryptedTokenEncoder createTokenEncoder() throws Exception { new GlobalScope(new CloudPath("/com.uid2.core/test/keysets/metadata.json"))); keysetKeyStore.loadContent(); - return new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + return new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); } - static JsonObject make1mOptOutEntryConfig() { + public static JsonObject make1mOptOutEntryConfig() { final JsonObject config = new JsonObject(); config.put(Const.Config.OptOutBloomFilterSizeProp, 100000); // 1:10 bloomfilter config.put(Const.Config.OptOutHeapDefaultCapacityProp, 1000000); // 1MM record @@ -110,7 +113,7 @@ static JsonObject make1mOptOutEntryConfig() { return config; } - static ICloudStorage make1mOptOutEntryStorage(String salt, List out_generatedFiles) throws Exception { + public static ICloudStorage make1mOptOutEntryStorage(String salt, List out_generatedFiles) throws Exception { final InMemoryStorageMock storage = new InMemoryStorageMock(); final MessageDigest digest = MessageDigest.getInstance("SHA-256"); final int numEntriesPerPartition = 1000000; @@ -148,18 +151,20 @@ static ICloudStorage make1mOptOutEntryStorage(String salt, List out_gene return storage; } - static UserIdentity[] createUserIdentities() { + public static UserIdentity[] createUserIdentities() { UserIdentity[] arr = new UserIdentity[65536]; for (int i = 0; i < 65536; i++) { final byte[] id = new byte[33]; new Random().nextBytes(id); - arr[i] = new UserIdentity(IdentityScope.UID2, IdentityType.Email, id, 0, - Instant.now().minusSeconds(120), Instant.now().minusSeconds(60)); + arr[i] = new UserIdentity( + IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.Test, + id, 0, Instant.now().minusSeconds(120), Instant.now().minusSeconds(60) + ); } return arr; } - static PublisherIdentity createPublisherIdentity() throws Exception { + public static PublisherIdentity createPublisherIdentity() throws Exception { RotatingClientKeyProvider clients = new RotatingClientKeyProvider( new EmbeddedResourceStorage(Main.class), new GlobalScope(new CloudPath("/com.uid2.core/test/clients/metadata.json"))); @@ -173,11 +178,10 @@ static PublisherIdentity createPublisherIdentity() throws Exception { throw new IllegalStateException("embedded resource does not include any publisher key"); } - /** * In memory optout store. Initialize with everything. Does not support modification */ - static class StaticOptOutStore implements IOptOutStore { + private static class StaticOptOutStore implements IOptOutStore { private CloudSyncOptOutStore.OptOutStoreSnapshot snapshot; public StaticOptOutStore(ICloudStorage storage, JsonObject jsonConfig, Collection partitions) throws CloudStorageException, IOException { @@ -189,8 +193,7 @@ public StaticOptOutStore(ICloudStorage storage, JsonObject jsonConfig, Collectio @Override public Instant getLatestEntry(UserIdentity firstLevelHashIdentity) { long epochSecond = this.snapshot.getOptOutTimestamp(firstLevelHashIdentity.id); - Instant instant = epochSecond > 0 ? Instant.ofEpochSecond(epochSecond) : null; - return instant; + return epochSecond > 0 ? Instant.ofEpochSecond(epochSecond) : null; } @Override @@ -203,5 +206,4 @@ public long getOptOutTimestampByAdId(String adId) { return -1; } } - } From 758ddf298b144cdb232f2bdd624cfb0582c7eb58 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 28 Aug 2025 14:23:04 +0800 Subject: [PATCH 05/38] Fixed tests --- src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 895389ab3..da7c7f3ff 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -772,6 +772,7 @@ void keyLatestHideRefreshKey(Vertx vertx, VertxTestContext testContext) { new KeysetKey(102, "key102".getBytes(), now, now, now.plusSeconds(10), RefreshKeysetId), new KeysetKey(103, "key103".getBytes(), now, now, now.plusSeconds(10), 10), }; + MultipleKeysetsTests test = new MultipleKeysetsTests(Arrays.asList(keysets), Arrays.asList(encryptionKeys)); Arrays.sort(encryptionKeys, Comparator.comparing(KeysetKey::getId)); send(vertx, "v2/key/latest", null, 200, respJson -> { @@ -2341,7 +2342,7 @@ void identityMapBatchEmailHashes(Vertx vertx, VertxTestContext testContext) { } send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, email_hashes); + checkIdentityMapResponse(json, emailHashes); testContext.completeNow(); }); } From 5f5ecefd0520fe648ae3bd2a750fd780153dded4 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 2 Sep 2025 18:55:31 +0800 Subject: [PATCH 06/38] Updated identity env to pass directly into UID2 generation instead --- .../com/uid2/operator/model/UserIdentity.java | 5 +-- .../service/EncryptedTokenEncoder.java | 12 +++---- .../com/uid2/operator/service/InputUtil.java | 1 - .../com/uid2/operator/service/TokenUtils.java | 12 +++++-- .../operator/service/UIDOperatorService.java | 29 ++++++++-------- .../operator/vertx/UIDOperatorVerticle.java | 2 +- .../com/uid2/operator/TokenEncodingTest.java | 8 ++--- .../uid2/operator/UIDOperatorServiceTest.java | 3 +- .../operator/UIDOperatorVerticleTest.java | 34 +++++++++---------- .../operator/benchmark/BenchmarkCommon.java | 6 ++-- 10 files changed, 55 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/uid2/operator/model/UserIdentity.java b/src/main/java/com/uid2/operator/model/UserIdentity.java index 3529ce008..fde02362a 100644 --- a/src/main/java/com/uid2/operator/model/UserIdentity.java +++ b/src/main/java/com/uid2/operator/model/UserIdentity.java @@ -6,18 +6,16 @@ public class UserIdentity { public final IdentityScope identityScope; public final IdentityType identityType; - public final IdentityEnvironment identityEnvironment; public final byte[] id; public final int privacyBits; public final Instant establishedAt; public final Instant refreshedAt; - public UserIdentity(IdentityScope identityScope, IdentityType identityType, IdentityEnvironment identityEnvironment, + public UserIdentity(IdentityScope identityScope, IdentityType identityType, byte[] id, int privacyBits, Instant establishedAt, Instant refreshedAt) { this.identityScope = identityScope; this.identityType = identityType; - this.identityEnvironment = identityEnvironment; this.id = id; this.privacyBits = privacyBits; this.establishedAt = establishedAt; @@ -27,7 +25,6 @@ public UserIdentity(IdentityScope identityScope, IdentityType identityType, Iden public boolean matches(UserIdentity that) { return this.identityScope.equals(that.identityScope) && this.identityType.equals(that.identityType) && - this.identityEnvironment.equals(that.identityEnvironment) && Arrays.equals(this.id, that.id); } } diff --git a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java index e4d6763fe..3e2b6e875 100644 --- a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java +++ b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java @@ -17,11 +17,9 @@ public class EncryptedTokenEncoder implements ITokenEncoder { private final KeyManager keyManager; - private final IdentityEnvironment identityEnvironment; - public EncryptedTokenEncoder(KeyManager keyManager, IdentityEnvironment identityEnvironment) { + public EncryptedTokenEncoder(KeyManager keyManager) { this.keyManager = keyManager; - this.identityEnvironment = identityEnvironment; } public byte[] encode(AdvertisingToken t, Instant asOf) { @@ -126,7 +124,7 @@ private RefreshToken decodeRefreshTokenV2(Buffer b) { TokenVersion.V2, createdAt, validTill, new OperatorIdentity(0, OperatorType.Service, 0, 0), new PublisherIdentity(siteId, 0, 0), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, this.identityEnvironment, identity, privacyBits, Instant.ofEpochMilli(establishedMillis), null)); + new UserIdentity(IdentityScope.UID2, IdentityType.Email, identity, privacyBits, Instant.ofEpochMilli(establishedMillis), null)); } private RefreshToken decodeRefreshTokenV3(Buffer b, byte[] bytes) { @@ -159,7 +157,7 @@ private RefreshToken decodeRefreshTokenV3(Buffer b, byte[] bytes) { return new RefreshToken( TokenVersion.V3, createdAt, expiresAt, operatorIdentity, publisherIdentity, - new UserIdentity(identityScope, identityType, this.identityEnvironment, id, privacyBits, establishedAt, null)); + new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, null)); } @Override @@ -227,7 +225,7 @@ public AdvertisingToken decodeAdvertisingTokenV2(Buffer b) { Instant.ofEpochMilli(expiresMillis), new OperatorIdentity(0, OperatorType.Service, 0, masterKeyId), new PublisherIdentity(siteId, siteKeyId, 0), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, this.identityEnvironment, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null) + new UserIdentity(IdentityScope.UID2, IdentityType.Email, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null) ); } catch (Exception e) { @@ -265,7 +263,7 @@ public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, Tok return new AdvertisingToken( tokenVersion, createdAt, expiresAt, operatorIdentity, publisherIdentity, - new UserIdentity(identityScope, identityType, this.identityEnvironment, id, privacyBits, establishedAt, refreshedAt) + new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, refreshedAt) ); } diff --git a/src/main/java/com/uid2/operator/service/InputUtil.java b/src/main/java/com/uid2/operator/service/InputUtil.java index 47c075534..ee2ea0ca9 100644 --- a/src/main/java/com/uid2/operator/service/InputUtil.java +++ b/src/main/java/com/uid2/operator/service/InputUtil.java @@ -268,7 +268,6 @@ public UserIdentity toUserIdentity(IdentityScope identityScope, IdentityEnvironm return new UserIdentity( identityScope, this.identityType, - identityEnvironment, getIdentityInput(), privacyBits, establishedAt, diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index 527ef5a53..f44e08fff 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -63,13 +63,19 @@ public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), encryptingKey.salt()); } - public static byte encodeIdentityScope(IdentityScope identityScope) { return (byte) (identityScope.value << 4); } + public static byte encodeIdentityScope(IdentityScope identityScope) { + return (byte) (identityScope.value << 4); + } public static byte encodeIdentityType(IdentityType identityType) { return (byte) (identityType.value << 2); } - public static byte encodeIdentityVersion(IdentityVersion identityVersion) { return (byte) (identityVersion.value << 6); } + public static byte encodeIdentityVersion(IdentityVersion identityVersion) { + return (byte) (identityVersion.value << 6); + } - public static byte encodeIdentityEnvironment(IdentityEnvironment identityEnvironment) { return (byte) (identityEnvironment.value); } + public static byte encodeIdentityEnvironment(IdentityEnvironment identityEnvironment) { + return (byte) (identityEnvironment.value); + } } diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index bd3a7b2fe..3cc3d1f2e 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -66,17 +66,17 @@ public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, this.saltRetrievalResponseHandler = saltRetrievalResponseHandler; this.uidInstanceIdProvider = uidInstanceIdProvider; - this.testOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, identityEnvironment, + this.testOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, InputUtil.normalizeEmail(OptOutIdentityForEmail).getIdentityInput(), Instant.now()); - this.testOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, identityEnvironment, + this.testOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, InputUtil.normalizePhone(OptOutIdentityForPhone).getIdentityInput(), Instant.now()); - this.testValidateIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, identityEnvironment, + this.testValidateIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, InputUtil.normalizeEmail(ValidateIdentityForEmail).getIdentityInput(), Instant.now()); - this.testValidateIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, identityEnvironment, + this.testValidateIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, InputUtil.normalizePhone(ValidateIdentityForPhone).getIdentityInput(), Instant.now()); - this.testRefreshOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, identityEnvironment, + this.testRefreshOptOutIdentityForEmail = getFirstLevelHashIdentity(identityScope, IdentityType.Email, InputUtil.normalizeEmail(RefreshOptOutIdentityForEmail).getIdentityInput(), Instant.now()); - this.testRefreshOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, identityEnvironment, + this.testRefreshOptOutIdentityForPhone = getFirstLevelHashIdentity(identityScope, IdentityType.Phone, InputUtil.normalizePhone(RefreshOptOutIdentityForPhone).getIdentityInput(), Instant.now()); this.operatorIdentity = new OperatorIdentity(0, OperatorType.Service, 0, 0); @@ -103,7 +103,7 @@ public IdentityTokens generateIdentity(IdentityRequest request, Duration refresh final Instant now = EncodingUtils.NowUTCMillis(this.clock); final byte[] firstLevelHash = getFirstLevelHash(request.userIdentity.id, now); final UserIdentity firstLevelHashIdentity = new UserIdentity( - request.userIdentity.identityScope, request.userIdentity.identityType, request.userIdentity.identityEnvironment, + request.userIdentity.identityScope, request.userIdentity.identityType, firstLevelHash, request.userIdentity.privacyBits, request.userIdentity.establishedAt, request.userIdentity.refreshedAt ); @@ -119,7 +119,7 @@ public IdentityTokens generateIdentity(IdentityRequest request, Duration refresh public RefreshResponse refreshIdentity(RefreshToken token, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) { this.validateTokenDurations(refreshIdentityAfter, refreshExpiresAfter, identityExpiresAfter); // should not be possible as different scopes/environments should be using different keys, but just in case - if (token.userIdentity.identityScope != this.identityScope || token.userIdentity.identityEnvironment != this.identityEnvironment) { + if (token.userIdentity.identityScope != this.identityScope) { return RefreshResponse.Invalid; } @@ -211,12 +211,12 @@ public boolean advertisingTokenMatches(String advertisingToken, UserIdentity use } private UserIdentity getFirstLevelHashIdentity(UserIdentity userIdentity, Instant asOf) { - return getFirstLevelHashIdentity(userIdentity.identityScope, userIdentity.identityType, userIdentity.identityEnvironment, userIdentity.id, asOf); + return getFirstLevelHashIdentity(userIdentity.identityScope, userIdentity.identityType, userIdentity.id, asOf); } - private UserIdentity getFirstLevelHashIdentity(IdentityScope identityScope, IdentityType identityType, IdentityEnvironment identityEnvironment, byte[] identityHash, Instant asOf) { + private UserIdentity getFirstLevelHashIdentity(IdentityScope identityScope, IdentityType identityType, byte[] identityHash, Instant asOf) { final byte[] firstLevelHash = getFirstLevelHash(identityHash, asOf); - return new UserIdentity(identityScope, identityType, identityEnvironment, firstLevelHash, 0, null, null); + return new UserIdentity(identityScope, identityType, firstLevelHash, 0, null, null); } private byte[] getFirstLevelHash(byte[] identityHash, Instant asOf) { @@ -244,7 +244,7 @@ private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt : TokenUtils.getAdvertisingIdV2(firstLevelHashIdentity.id, salt); } else { try { - return TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.identityEnvironment, firstLevelHashIdentity.id, key); + return TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, this.identityEnvironment, firstLevelHashIdentity.id, key); } catch (Exception e) { LOGGER.error("Exception when generating V4 advertising ID", e); return null; @@ -281,7 +281,7 @@ private IdentityTokens generateIdentity(PublisherIdentity publisherIdentity, Use final MappedIdentity mappedIdentity = getMappedIdentity(firstLevelHashIdentity, nowUtc); final UserIdentity advertisingIdentity = new UserIdentity( - firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.identityEnvironment, + firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, mappedIdentity.advertisingId, firstLevelHashIdentity.privacyBits, firstLevelHashIdentity.establishedAt, nowUtc ); @@ -314,8 +314,7 @@ protected static class GlobalOptoutResult { private final Instant time; // providedTime can be null if isOptedOut is false! - GlobalOptoutResult(Instant providedTime) - { + GlobalOptoutResult(Instant providedTime) { isOptedOut = providedTime != null; time = providedTime; } diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 6afa43ba4..e6103ded9 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -184,7 +184,7 @@ public UIDOperatorVerticle(IConfigStore configStore, this.optOutStore = optOutStore; this.clock = clock; this.identityEnvironment = IdentityEnvironment.fromString(config.getString("identity_environment", "test")); - this.encoder = new EncryptedTokenEncoder(keyManager, identityEnvironment); + this.encoder = new EncryptedTokenEncoder(keyManager); this.encryptedPayloadHandler = new V2PayloadHandler(keyManager, config.getBoolean("enable_v2_encryption", true), this.identityScope, siteProvider); this.phoneSupport = config.getBoolean("enable_phone_support", true); this.tcfVendorId = config.getInteger("tcf_vendor_id", 21); diff --git a/src/test/java/com/uid2/operator/TokenEncodingTest.java b/src/test/java/com/uid2/operator/TokenEncodingTest.java index cf8392749..58277c188 100644 --- a/src/test/java/com/uid2/operator/TokenEncodingTest.java +++ b/src/test/java/com/uid2/operator/TokenEncodingTest.java @@ -47,7 +47,7 @@ public TokenEncodingTest() throws Exception { @ParameterizedTest @EnumSource(value = TokenVersion.class, names = {"V3", "V4"}) void testRefreshTokenEncoding(TokenVersion tokenVersion) { - final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager, IdentityEnvironment.Test); + final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager); final Instant now = EncodingUtils.NowUTCMillis(); final byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity("test@example.com", "some-salt"); @@ -57,7 +57,7 @@ void testRefreshTokenEncoding(TokenVersion tokenVersion) { now.plusSeconds(360), new OperatorIdentity(101, OperatorType.Service, 102, 103), new PublisherIdentity(111, 112, 113), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.Test, firstLevelHash, 121, now, now.minusSeconds(122)) + new UserIdentity(IdentityScope.UID2, IdentityType.Email, firstLevelHash, 121, now, now.minusSeconds(122)) ); if (tokenVersion == TokenVersion.V4) { @@ -96,7 +96,7 @@ void testRefreshTokenEncoding(TokenVersion tokenVersion) { "true, V2", }) void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVersion) { - final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager, IdentityEnvironment.Test); + final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager); final Instant now = EncodingUtils.NowUTCMillis(); final byte[] rawUid = UIDOperatorVerticleTest.getRawUid(IdentityType.Email, "test@example.com", IdentityScope.UID2, useRawUIDv3); @@ -107,7 +107,7 @@ void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVers now.plusSeconds(60), new OperatorIdentity(101, OperatorType.Service, 102, 103), new PublisherIdentity(111, 112, 113), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.Test, rawUid, 121, now, now.minusSeconds(122)) + new UserIdentity(IdentityScope.UID2, IdentityType.Email, rawUid, 121, now, now.minusSeconds(122)) ); final byte[] encodedBytes = encoder.encode(token, now); diff --git a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java index 99a0168ea..118c330b9 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java @@ -88,7 +88,7 @@ void setup() throws Exception { "/com.uid2.core/test/salts/metadata.json"); saltProvider.loadContent(); - tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); setNow(Instant.now()); @@ -167,7 +167,6 @@ private UserIdentity createUserIdentity(String rawIdentityHash, IdentityScope sc return new UserIdentity( scope, type, - environment, rawIdentityHash.getBytes(StandardCharsets.UTF_8), 0, this.now.minusSeconds(234), diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index da7c7f3ff..f10580d19 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -1452,7 +1452,7 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi decodeV2RefreshToken(json); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, identityType); RefreshToken refreshToken = encoder.decodeRefreshToken(body.getString("decrypted_refresh_token")); @@ -1557,7 +1557,7 @@ void tokenGenerateForEmail(String contentType, Vertx vertx, VertxTestContext tes assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -1597,7 +1597,7 @@ void tokenGenerateForEmailHash(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -1646,7 +1646,7 @@ void tokenGenerateThenRefresh(String contentType, Vertx vertx, VertxTestContext assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Email); @@ -1706,7 +1706,7 @@ void tokenGenerateThenRefreshSaltsExpired(Vertx vertx, VertxTestContext testCont assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Email); @@ -1874,7 +1874,7 @@ void tokenGenerateUsingCustomSiteKey(Vertx vertx, VertxTestContext testContext) assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); @@ -1905,7 +1905,7 @@ void tokenGenerateSaltsExpired(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -2639,7 +2639,7 @@ void tokenGenerateForPhone(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Phone); @@ -2676,7 +2676,7 @@ void tokenGenerateForPhoneHash(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Phone); @@ -2718,7 +2718,7 @@ void tokenGenerateThenRefreshForPhone(Vertx vertx, VertxTestContext testContext) assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Phone); @@ -3442,7 +3442,7 @@ void cstgDomainNameCheckPasses(String httpOrigin, Vertx vertx, VertxTestContext JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct testContext.completeNow(); }); @@ -3469,7 +3469,7 @@ void cstgAppNameCheckPasses(String appName, Vertx vertx, VertxTestContext testCo JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct assertTokenStatusMetrics( clientSideTokenGenerateSiteId, @@ -3992,7 +3992,7 @@ void cstgUserOptsOutAfterTokenGenerate(String id, IdentityType identityType, Ver when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(null); - final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(UserIdentity.class); sendCstg(vertx, @@ -4069,7 +4069,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id assertNotNull(genBody); decodeV2RefreshToken(respJson); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); @@ -4092,7 +4092,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); //make sure the new advertising token from refresh looks right AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder2, refreshBody, identityType); @@ -4139,7 +4139,7 @@ void cstgSaltsExpired(String httpOrigin, Vertx vertx, VertxTestContext testConte JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(true); @@ -4452,7 +4452,7 @@ void tokenGenerateRotatingKeysets_GENERATOR(String testRun, Vertx vertx, VertxTe assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); diff --git a/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java b/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java index 4a4c9a52a..d05674558 100644 --- a/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java +++ b/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java @@ -69,7 +69,7 @@ public static IUIDOperatorService createUidOperatorService() throws Exception { "/com.uid2.core/test/salts/metadata.json"); saltProvider.loadContent(); - final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + final EncryptedTokenEncoder tokenEncoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); final List optOutPartitionFiles = new ArrayList<>(); final ICloudStorage optOutLocalStorage = make1mOptOutEntryStorage( saltProvider.getSnapshot(Instant.now()).getFirstLevelSalt(), @@ -100,7 +100,7 @@ public static EncryptedTokenEncoder createTokenEncoder() throws Exception { new GlobalScope(new CloudPath("/com.uid2.core/test/keysets/metadata.json"))); keysetKeyStore.loadContent(); - return new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider), IdentityEnvironment.Test); + return new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); } public static JsonObject make1mOptOutEntryConfig() { @@ -157,7 +157,7 @@ public static UserIdentity[] createUserIdentities() { final byte[] id = new byte[33]; new Random().nextBytes(id); arr[i] = new UserIdentity( - IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.Test, + IdentityScope.UID2, IdentityType.Email, id, 0, Instant.now().minusSeconds(120), Instant.now().minusSeconds(60) ); } From 30f8303d86185aa917d79b31e1f35547e3254b65 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 2 Sep 2025 19:25:57 +0800 Subject: [PATCH 07/38] Added /v3/identity/map tests --- .../com/uid2/operator/service/TokenUtils.java | 4 + .../operator/EUIDOperatorVerticleTest.java | 20 +- .../com/uid2/operator/TokenEncodingTest.java | 4 +- .../uid2/operator/UIDOperatorServiceTest.java | 8 +- .../operator/UIDOperatorVerticleTest.java | 210 ++++++++++++------ 5 files changed, 167 insertions(+), 79 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index f44e08fff..0bd89e284 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -63,6 +63,10 @@ public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), encryptingKey.salt()); } + public static byte[] getAdvertisingIdV4FromIdentity(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identity, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) throws Exception { + return getAdvertisingIdV4(scope, type, environment, getFirstLevelHashFromIdentity(identity, firstLevelSalt), encryptingKey); + } + public static byte encodeIdentityScope(IdentityScope identityScope) { return (byte) (identityScope.value << 4); } diff --git a/src/test/java/com/uid2/operator/EUIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/EUIDOperatorVerticleTest.java index 98653875f..d9a4ef419 100644 --- a/src/test/java/com/uid2/operator/EUIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/EUIDOperatorVerticleTest.java @@ -1,6 +1,5 @@ package com.uid2.operator; -import com.uid2.shared.model.TokenVersion; import org.junit.jupiter.api.Test; import com.uid2.operator.model.IdentityScope; @@ -10,18 +9,19 @@ import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxTestContext; -import java.io.IOException; - import static org.junit.jupiter.api.Assertions.*; -public class EUIDOperatorVerticleTest extends UIDOperatorVerticleTest { - public EUIDOperatorVerticleTest() throws IOException { +class EUIDOperatorVerticleTest extends UIDOperatorVerticleTest { + @Override + protected IdentityScope getIdentityScope() { + return IdentityScope.EUID; } @Override - protected IdentityScope getIdentityScope() { return IdentityScope.EUID; } - @Override - protected boolean useRawUidV3() { return true; } + protected boolean useRawUidV3() { + return true; + } + @Override protected void addAdditionalTokenGenerateParams(JsonObject payload) { if (payload != null && !payload.containsKey("tcf_consent_string")) { @@ -35,7 +35,7 @@ void badRequestOnInvalidTcfConsent(Vertx vertx, VertxTestContext testContext) { fakeAuth(clientSiteId, Role.GENERATOR); setupSalts(); setupKeys(); - + final String emailAddress = "test@uid2.com"; final JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -53,7 +53,7 @@ void noTCFString(Vertx vertx, VertxTestContext testContext) { final String emailAddress = "test@uid2.com"; final JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); - sendTokenGenerate(vertx, v2Payload, 200, json -> testContext.completeNow(), false); + sendTokenGenerate(vertx, v2Payload, 200, json -> testContext.completeNow(), false); } diff --git a/src/test/java/com/uid2/operator/TokenEncodingTest.java b/src/test/java/com/uid2/operator/TokenEncodingTest.java index 58277c188..e7816776d 100644 --- a/src/test/java/com/uid2/operator/TokenEncodingTest.java +++ b/src/test/java/com/uid2/operator/TokenEncodingTest.java @@ -93,13 +93,13 @@ void testRefreshTokenEncoding(TokenVersion tokenVersion) { "false, V3", "true, V3", "false, V2", - "true, V2", + "true, V2" }) void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVersion) { final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager); final Instant now = EncodingUtils.NowUTCMillis(); - final byte[] rawUid = UIDOperatorVerticleTest.getRawUid(IdentityType.Email, "test@example.com", IdentityScope.UID2, useRawUIDv3); + final byte[] rawUid = UIDOperatorVerticleTest.getRawUid(IdentityScope.UID2, IdentityType.Email, "test@example.com", useRawUIDv3); final AdvertisingToken token = new AdvertisingToken( adTokenVersion, diff --git a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java index 118c330b9..2ee727b75 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java @@ -919,8 +919,8 @@ void testMappedIdentityWithPreviousSaltReturnsPreviousUid() { MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); - var expectedCurrentUID = UIDOperatorVerticleTest.getRawUid(IdentityType.Email, email, FIRST_LEVEL_SALT, salt.currentSalt(), IdentityScope.UID2, uid2Config.getBoolean(IdentityV3Prop)); - var expectedPreviousUID = UIDOperatorVerticleTest.getRawUid(IdentityType.Email, email, FIRST_LEVEL_SALT, salt.previousSalt(), IdentityScope.UID2, uid2Config.getBoolean(IdentityV3Prop)); + var expectedCurrentUID = UIDOperatorVerticleTest.getRawUid(IdentityScope.UID2, IdentityType.Email, email, FIRST_LEVEL_SALT, salt.currentSalt(), uid2Config.getBoolean(IdentityV3Prop)); + var expectedPreviousUID = UIDOperatorVerticleTest.getRawUid(IdentityScope.UID2, IdentityType.Email, email, FIRST_LEVEL_SALT, salt.previousSalt(), uid2Config.getBoolean(IdentityV3Prop)); assertArrayEquals(expectedCurrentUID, mappedIdentity.advertisingId); assertArrayEquals(expectedPreviousUID, mappedIdentity.previousAdvertisingId); } @@ -941,7 +941,7 @@ void testMappedIdentityWithOutdatedPreviousSaltReturnsNoPreviousUid(long extraMs MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, IdentityEnvironment.Test, 0, this.now), OptoutCheckPolicy.RespectOptOut, now); MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); - var expectedCurrentUID = UIDOperatorVerticleTest.getRawUid(IdentityType.Email, email, FIRST_LEVEL_SALT, salt.currentSalt(), IdentityScope.UID2, uid2Config.getBoolean(IdentityV3Prop)); + var expectedCurrentUID = UIDOperatorVerticleTest.getRawUid(IdentityScope.UID2, IdentityType.Email, email, FIRST_LEVEL_SALT, salt.currentSalt(), uid2Config.getBoolean(IdentityV3Prop)); assertArrayEquals(expectedCurrentUID, mappedIdentity.advertisingId); assertArrayEquals(null , mappedIdentity.previousAdvertisingId); } @@ -962,7 +962,7 @@ void testMappedIdentityWithNoPreviousSaltReturnsNoPreviousUid() { MappedIdentity mappedIdentity = uid2Service.mapIdentity(mapRequest); - var expectedCurrentUID = UIDOperatorVerticleTest.getRawUid(IdentityType.Email, email, FIRST_LEVEL_SALT, salt.currentSalt(), IdentityScope.UID2, uid2Config.getBoolean(IdentityV3Prop)); + var expectedCurrentUID = UIDOperatorVerticleTest.getRawUid(IdentityScope.UID2, IdentityType.Email, email, FIRST_LEVEL_SALT, salt.currentSalt(), uid2Config.getBoolean(IdentityV3Prop)); assertArrayEquals(expectedCurrentUID, mappedIdentity.advertisingId); assertArrayEquals(null, mappedIdentity.previousAdvertisingId); } diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index f10580d19..ae52f3973 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -59,18 +59,15 @@ import org.mockito.quality.Strictness; import org.slf4j.LoggerFactory; -import static java.time.temporal.ChronoUnit.DAYS; -import static org.assertj.core.api.Assertions.*; - import javax.crypto.SecretKey; import java.math.BigInteger; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.*; -import java.time.*; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; -import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -80,6 +77,8 @@ import static com.uid2.operator.vertx.UIDOperatorVerticle.*; import static com.uid2.shared.Const.Data.*; import static com.uid2.shared.Const.Http.ClientVersionHeader; +import static java.time.temporal.ChronoUnit.DAYS; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -87,7 +86,6 @@ @ExtendWith({VertxExtension.class, MockitoExtension.class}) @MockitoSettings(strictness = Strictness.LENIENT) public class UIDOperatorVerticleTest { - private final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); private static final Instant legacyClientCreationDateTime = Instant.ofEpochSecond(OPT_OUT_CHECK_CUTOFF_DATE).minus(1, ChronoUnit.SECONDS); private static final Instant newClientCreationDateTime = Instant.ofEpochSecond(OPT_OUT_CHECK_CUTOFF_DATE).plus(1, ChronoUnit.SECONDS); private static final String firstLevelSalt = "first-level-salt"; @@ -105,9 +103,10 @@ public class UIDOperatorVerticleTest { private static final String iosClientVersionHeaderValue = "ios-1.2.3"; private static final String tvosClientVersionHeaderValue = "tvos-1.2.3"; private static final int clientSideTokenGenerateSiteId = 123; - private static final int optOutStatusMaxRequestSize = 1000; + private final Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + @Mock private ISiteStore siteProvider; @Mock @@ -144,7 +143,7 @@ public class UIDOperatorVerticleTest { private final JsonObject config = new JsonObject(); @BeforeEach - public void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo testInfo) { + void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo testInfo) { when(saltProvider.getSnapshot(any())).thenReturn(saltProviderSnapshot); when(saltProviderSnapshot.getExpires()).thenReturn(Instant.now().plus(1, ChronoUnit.HOURS)); when(clock.instant()).thenAnswer(i -> now); @@ -166,7 +165,6 @@ public void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo t this.uidInstanceIdProvider = new UidInstanceIdProvider("test-instance", "id"); this.uidOperatorVerticle = new ExtendedUIDOperatorVerticle(configStore, config, config.getBoolean("client_side_token_generate"), siteProvider, clientKeyProvider, clientSideKeypairProvider, new KeyManager(keysetKeyStore, keysetProvider), saltProvider, optOutStore, clock, statsCollectorQueue, secureLinkValidatorService, shutdownHandler::handleSaltRetrievalResponse, uidInstanceIdProvider); - vertx.deployVerticle(uidOperatorVerticle, testContext.succeeding(id -> testContext.completeNow())); this.registry = new SimpleMeterRegistry(); @@ -174,7 +172,7 @@ public void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo t } @AfterEach - public void teardown() throws Exception { + void teardown() { Metrics.globalRegistry.remove(registry); } @@ -232,14 +230,6 @@ private void clearAuth() { when(clientKeyProvider.get(any())).thenReturn(null); } - private static String urlEncode(String value) { - try { - return URLEncoder.encode(value, StandardCharsets.UTF_8); - } catch (Exception e) { - return null; - } - } - private String getUrlForEndpoint(String endpoint) { return String.format("http://127.0.0.1:%d/%s", Const.Port.ServicePortForOperator + Utils.getPortOffset(), endpoint); } @@ -297,7 +287,6 @@ private void sendTokenGenerate(Vertx vertx, JsonObject v2PostPayload, int expect } private void sendTokenGenerate(Vertx vertx, JsonObject v2PostPayload, int expectedHttpCode, String referer, Handler handler, boolean additionalParams, Map additionalHeaders) { - ClientKey ck = (ClientKey) clientKeyProvider.get(""); long nonce = new BigInteger(Random.getBytes(8)).longValue(); @@ -399,10 +388,6 @@ private JsonObject tryParseResponse(HttpResponse resp) { } } - private void postV2(ClientKey ck, Vertx vertx, String endpoint, JsonObject body, long nonce, String referer, Handler>> handler) { - postV2(ck, vertx, endpoint, body, nonce, referer, handler, Collections.emptyMap(), false); - } - private void postV2(ClientKey ck, Vertx vertx, String endpoint, JsonObject body, long nonce, String referer, Handler>> handler, Map additionalHeaders) { postV2(ck, vertx, endpoint, body, nonce, referer, handler, additionalHeaders, false); } @@ -497,7 +482,7 @@ private enum KeyDownloadEndpoint { SHARING("/key/sharing"), BIDSTREAM("/key/bidstream"); - private String path; + private final String path; KeyDownloadEndpoint(String path) { this.path = path; @@ -636,21 +621,29 @@ private void assertTokenStatusMetrics(Integer siteId, TokenResponseStatsCollecto } private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, String rotatingSalt) { - return getRawUid(identityType, identityString, firstLevelSalt, rotatingSalt, getIdentityScope(), useRawUidV3()); + return getRawUid(getIdentityScope(), identityType, identityString, firstLevelSalt, rotatingSalt, useRawUidV3()); + } + + private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) throws Exception { + return getRawUidV4(getIdentityScope(), identityType, IdentityEnvironment.Test, identityString, firstLevelSalt, rotatingKey); } - public static byte[] getRawUid(IdentityType identityType, String identityString, String firstLevelSalt, String rotatingSalt, IdentityScope identityScope, boolean useRawUidV3) { + public static byte[] getRawUid(IdentityScope identityScope, IdentityType identityType, String identityString, String firstLevelSalt, String rotatingSalt, boolean useRawUidV3) { return !useRawUidV3 ? TokenUtils.getAdvertisingIdV2FromIdentity(identityString, firstLevelSalt, rotatingSalt) : TokenUtils.getAdvertisingIdV3FromIdentity(identityScope, identityType, identityString, firstLevelSalt, rotatingSalt); } - public static byte[] getRawUid(IdentityType identityType, String identityString, IdentityScope identityScope, boolean useRawUidV3) { + public static byte[] getRawUid(IdentityScope identityScope, IdentityType identityType, String identityString, boolean useRawUidV3) { return !useRawUidV3 ? TokenUtils.getAdvertisingIdV2FromIdentity(identityString, firstLevelSalt, rotatingSalt123.currentSalt()) : TokenUtils.getAdvertisingIdV3FromIdentity(identityScope, identityType, identityString, firstLevelSalt, rotatingSalt123.currentSalt()); } + public static byte[] getRawUidV4(IdentityScope identityScope, IdentityType identityType, IdentityEnvironment identityEnvironment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) throws Exception { + return TokenUtils.getAdvertisingIdV4FromIdentity(identityScope, identityType, identityEnvironment, identityString, firstLevelSalt, rotatingKey); + } + private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, String rotatingSalt) { return !useRawUidV3() ? TokenUtils.getAdvertisingIdV2FromIdentityHash(identityString, firstLevelSalt, rotatingSalt) @@ -698,10 +691,7 @@ void verticleDeployed(Vertx vertx, VertxTestContext testContext) { } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" - }) + @ValueSource(strings = {"text/plain", "application/octet-stream"}) void keyLatestNoAcl(String contentType, Vertx vertx, VertxTestContext testContext) { fakeAuth(5, Role.ID_READER); Keyset[] keysets = { @@ -879,10 +869,7 @@ RefreshToken decodeRefreshToken(EncryptedTokenEncoder encoder, String refreshTok } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" - }) + @ValueSource(strings = {"text/plain", "application/octet-stream"}) void identityMapNewClientNoPolicySpecified(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, newClientCreationDateTime, Role.MAPPER); @@ -1056,10 +1043,7 @@ void identityMapNewClientWrongPolicySpecifiedOlderKeySuccessful(String policyPar } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" - }) + @ValueSource(strings = {"text/plain", "application/octet-stream"}) void v3IdentityMapMixedInputSuccess(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); @@ -1285,6 +1269,118 @@ void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) }); } + @ParameterizedTest + @ValueSource(strings = {"text/plain", "application/octet-stream"}) + void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + fakeAuth(clientSiteId, Role.MAPPER); + + setupSalts(); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + + SaltEntry salt = new SaltEntry( + 1, + "1", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + "salt123", + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + null); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + + JsonObject request = new JsonObject(""" + { + "email": ["test1@uid2.com"] + } + """); + + send(vertx, "v3/identity/map", request, 200, respJson -> { + JsonObject body = respJson.getJsonObject("body"); + assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); + + var mappedEmails = body.getJsonArray("email"); + assertEquals(1, mappedEmails.size()); + + try { + var mappedEmailExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKey())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousSalt())), + "r", refreshFrom.getEpochSecond() + ); + assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + assertEquals(0, body.getJsonArray("email_hash").size()); + assertEquals(0, body.getJsonArray("phone").size()); + var mappedPhoneHash = body.getJsonArray("phone_hash"); + assertEquals(0, mappedPhoneHash.size()); + + assertEquals("success", respJson.getString("status")); + testContext.completeNow(); + }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); + } + + @ParameterizedTest + @ValueSource(strings = {"text/plain", "application/octet-stream"}) + void v3IdentityMapWithV4UidAndV4PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + fakeAuth(clientSiteId, Role.MAPPER); + + setupSalts(); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + + SaltEntry salt = new SaltEntry( + 1, + "1", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + null, + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + + JsonObject request = new JsonObject(""" + { + "email": ["test1@uid2.com"] + } + """); + + send(vertx, "v3/identity/map", request, 200, respJson -> { + JsonObject body = respJson.getJsonObject("body"); + assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); + + var mappedEmails = body.getJsonArray("email"); + assertEquals(1, mappedEmails.size()); + + try { + var mappedEmailExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKey())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousKey())), + "r", refreshFrom.getEpochSecond() + ); + assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + assertEquals(0, body.getJsonArray("email_hash").size()); + assertEquals(0, body.getJsonArray("phone").size()); + var mappedPhoneHash = body.getJsonArray("phone_hash"); + assertEquals(0, mappedPhoneHash.size()); + + assertEquals("success", respJson.getString("status")); + testContext.completeNow(); + }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); + } + @Test void tokenGenerateNewClientNoPolicySpecified(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; @@ -1478,8 +1574,7 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi TokenResponseStatsCollector.ResponseStatus.Success, TokenResponseStatsCollector.PlatformType.Other); - sendTokenRefresh(vertx, testContext, body.getString("refresh_token"), body.getString("refresh_response_key"), 200, refreshRespJson -> - { + sendTokenRefresh(vertx, testContext, body.getString("refresh_token"), body.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNull(refreshBody); @@ -1619,10 +1714,7 @@ void tokenGenerateForEmailHash(Vertx vertx, VertxTestContext testContext) { } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" - }) + @ValueSource(strings = {"text/plain", "application/octet-stream"}) void tokenGenerateThenRefresh(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; @@ -1701,8 +1793,7 @@ void tokenGenerateThenRefreshSaltsExpired(Vertx vertx, VertxTestContext testCont when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> - { + sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); @@ -2131,10 +2222,7 @@ void tokenRefreshOptOutBeforeLogin(Vertx vertx, VertxTestContext testContext) { } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" - }) + @ValueSource(strings = {"text/plain", "application/octet-stream"}) void tokenValidateWithEmail_Mismatch(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = ValidateIdentityForEmail; @@ -2505,10 +2593,7 @@ void optOutStatusUnauthorized(String contentType, Vertx vertx, VertxTestContext } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" - }) + @ValueSource(strings = {"text/plain", "application/octet-stream"}) void logoutV2(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.OPTOUT); @@ -3216,7 +3301,7 @@ void cstgNoIdentityHashProvided(Vertx vertx, VertxTestContext testContext) throw @ParameterizedTest @CsvSource({ "https://blahblah.com", - "http://local1host:8080", //intentionally spelling localhost wrong here! + "http://local1host:8080" //intentionally spelling localhost wrong here! }) void cstgDomainNameCheckFails(String httpOrigin, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend(); @@ -3245,7 +3330,7 @@ void cstgDomainNameCheckFails(String httpOrigin, Vertx vertx, VertxTestContext t @CsvSource({ "''", // An empty quoted value results in the empty string. "com.123", - "com.", + "com." }) void cstgAppNameCheckFails(String appName, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend(Collections.emptyList(), List.of("com.123.Game.App.android")); @@ -3275,7 +3360,7 @@ void cstgAppNameCheckFails(String appName, Vertx vertx, VertxTestContext testCon @ParameterizedTest @CsvSource({ - "http://gototest.com", + "http://gototest.com" }) void cstgDomainNameCheckFailsAndLogInvalidHttpOrigin(String httpOrigin, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { ListAppender logWatcher = new ListAppender<>(); @@ -3353,7 +3438,6 @@ void cstgDisabledAsUnauthorized(Vertx vertx, VertxTestContext testContext) throw final SecretKey secretKey = ClientSideTokenGenerateTestUtil.deriveKey(serverPublicKey, clientPrivateKey); final long timestamp = Instant.now().toEpochMilli(); - JsonObject requestJson = new JsonObject(); requestJson.put("payload", ""); requestJson.put("iv", ""); @@ -3382,7 +3466,7 @@ void cstgDisabledAsUnauthorized(Vertx vertx, VertxTestContext testContext) throw @ParameterizedTest @CsvSource({ - "http://gototest.com", + "http://gototest.com" }) void cstgDomainNameCheckFailsAndLogSeveralInvalidHttpOrigin(String httpOrigin, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { ListAppender logWatcher = new ListAppender<>(); @@ -3425,7 +3509,7 @@ void cstgDomainNameCheckFailsAndLogSeveralInvalidHttpOrigin(String httpOrigin, V @CsvSource({ "https://cstg.co.uk", "https://cstg2.com", - "http://localhost:8080", + "http://localhost:8080" }) void cstgDomainNameCheckPasses(String httpOrigin, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk", "cstg2.com", "localhost"); @@ -3452,7 +3536,7 @@ void cstgDomainNameCheckPasses(String httpOrigin, Vertx vertx, VertxTestContext @CsvSource({ "com.123.Game.App.android", "com.123.game.app.android", - "123456789", + "123456789" }) void cstgAppNameCheckPasses(String appName, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend(Collections.emptyList(), List.of("com.123.Game.App.android", "123456789")); From 4feda12969f63e416d88c392937cbee5670bf6fc Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 2 Sep 2025 19:43:30 +0800 Subject: [PATCH 08/38] Added /v2/token/generate tests --- .../operator/UIDOperatorVerticleTest.java | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index ae52f3973..41472f6b8 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -1274,7 +1274,6 @@ void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); var lastUpdated = Instant.now().minus(1, DAYS); @@ -1330,7 +1329,6 @@ void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTe void v3IdentityMapWithV4UidAndV4PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); var lastUpdated = Instant.now().minus(1, DAYS); @@ -1635,6 +1633,65 @@ void tokenGenerateOptOutTokenWithDisableOptoutTokenFF(String policyParameterKey, }); } + @ParameterizedTest + @ValueSource(strings = {"text/plain", "application/octet-stream"}) + void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + final String emailAddress = "test@uid2.com"; + fakeAuth(clientSiteId, Role.GENERATOR); + setupSalts(); + setupKeys(); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + + SaltEntry salt = new SaltEntry( + 1, + "1", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + null, + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + + JsonObject v2Payload = new JsonObject(); + v2Payload.put("email", emailAddress); + + sendTokenGenerate(vertx, v2Payload, 200, + json -> { + assertEquals("success", json.getString("status")); + JsonObject body = json.getJsonObject("body"); + assertNotNull(body); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); + + assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); + assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); + assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKey()), advertisingToken.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + RefreshToken refreshToken = decodeRefreshToken(encoder, body.getString("decrypted_refresh_token")); + assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); + assertArrayEquals(TokenUtils.getFirstLevelHashFromIdentity(emailAddress, firstLevelSalt), refreshToken.userIdentity.id); + + assertEqualsClose(now.plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(body.getLong("identity_expires")), 10); + assertEqualsClose(now.plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(body.getLong("refresh_expires")), 10); + assertEqualsClose(now.plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(body.getLong("refresh_from")), 10); + + assertStatsCollector("/v2/token/generate", null, "test-contact", clientSiteId); + + testContext.completeNow(); + }, + Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); + } + @ParameterizedTest @ValueSource(strings = {"text/plain", "application/octet-stream"}) void tokenGenerateForEmail(String contentType, Vertx vertx, VertxTestContext testContext) { From 6a2386a8caff08b8dee4ae9d3be9c5ef22ee9e01 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 2 Sep 2025 19:59:21 +0800 Subject: [PATCH 09/38] Updated /v3/identity/map tests --- .../operator/UIDOperatorVerticleTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 41472f6b8..3b21f73f8 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -1269,6 +1269,61 @@ void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) }); } + @ParameterizedTest + @ValueSource(strings = {"text/plain", "application/octet-stream"}) + void v3IdentityMapWithV4UidAndNoPrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + fakeAuth(clientSiteId, Role.MAPPER); + setupSalts(); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + + SaltEntry salt = new SaltEntry( + 1, + "1", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + null, + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + null); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + + JsonObject request = new JsonObject(""" + { + "email": ["test1@uid2.com"] + } + """); + + send(vertx, "v3/identity/map", request, 200, respJson -> { + JsonObject body = respJson.getJsonObject("body"); + assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); + + var mappedEmails = body.getJsonArray("email"); + assertEquals(1, mappedEmails.size()); + + try { + var mappedEmailExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKey())), + "p", null, + "r", refreshFrom.getEpochSecond() + ); + assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + assertEquals(0, body.getJsonArray("email_hash").size()); + assertEquals(0, body.getJsonArray("phone").size()); + var mappedPhoneHash = body.getJsonArray("phone_hash"); + assertEquals(0, mappedPhoneHash.size()); + + assertEquals("success", respJson.getString("status")); + testContext.completeNow(); + }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); + } + @ParameterizedTest @ValueSource(strings = {"text/plain", "application/octet-stream"}) void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { From 31f57ef66bbf8984863cce063f0adc6b82f5c255 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 2 Sep 2025 20:42:37 +0800 Subject: [PATCH 10/38] Added /v2/identity/map and /v2/token/validate tests --- .../operator/UIDOperatorVerticleTest.java | 200 ++++++++++++------ 1 file changed, 134 insertions(+), 66 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 3b21f73f8..3567a3e97 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -271,7 +271,6 @@ protected void sendTokenGenerate(Vertx vertx, JsonObject v2PostPayload, int expe sendTokenGenerate(vertx, v2PostPayload, expectedHttpCode, null, handler, true, Collections.emptyMap()); } - protected void sendTokenGenerate(Vertx vertx, JsonObject v2PostPayload, int expectedHttpCode, Handler handler, Map additionalHeaders) { sendTokenGenerate(vertx, v2PostPayload, expectedHttpCode, null, handler, true, additionalHeaders); @@ -358,7 +357,6 @@ private void sendTokenRefresh(Vertx vertx, VertxTestContext testContext, String handler.handle(tryParseResponse(response)); } }))); - } private String decodeV2RefreshToken(JsonObject respJson) { @@ -495,10 +493,12 @@ public String getPath() { private void checkIdentityMapResponse(JsonObject response, String... expectedIdentifiers) { assertEquals("success", response.getString("status")); + JsonObject body = response.getJsonObject("body"); JsonArray mapped = body.getJsonArray("mapped"); assertNotNull(mapped); assertEquals(expectedIdentifiers.length, mapped.size()); + for (int i = 0; i < expectedIdentifiers.length; ++i) { String expectedIdentifier = expectedIdentifiers[i]; JsonObject actualMap = mapped.getJsonObject(i); @@ -508,11 +508,86 @@ private void checkIdentityMapResponse(JsonObject response, String... expectedIde } } + private void checkIdentityMapResponseWithV4Uid(JsonObject response, SaltEntry salt, String... expectedIdentifiers) { + assertEquals("success", response.getString("status")); + + JsonObject body = response.getJsonObject("body"); + JsonArray mapped = body.getJsonArray("mapped"); + assertNotNull(mapped); + assertEquals(expectedIdentifiers.length, mapped.size()); + + for (int i = 0; i < expectedIdentifiers.length; ++i) { + String expectedIdentifier = expectedIdentifiers[i]; + JsonObject actualMap = mapped.getJsonObject(i); + assertEquals(expectedIdentifier, actualMap.getString("identifier")); + try { + assertEquals(actualMap.getString("advertising_id"), EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, expectedIdentifier, firstLevelSalt, salt.currentKey()))); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + assertFalse(actualMap.getString("bucket_id").isEmpty()); + } + } + protected void setupSalts() { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(rotatingSalt123); } + protected SaltEntry setupSaltForV4UidAndV4PrevUid() { + when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + SaltEntry salt = new SaltEntry( + 1, + "1", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + null, + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + return salt; + } + + protected SaltEntry setupSaltForV4UidAndV3PrevUid() { + when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + SaltEntry salt = new SaltEntry( + 1, + "1", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + "salt123", + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + null); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + return salt; + } + + protected SaltEntry setupSaltForV4UidAndNoPrevUid() { + when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + SaltEntry salt = new SaltEntry( + 1, + "1", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + null, + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + null); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + return salt; + } + private HashMap keysetsToMap(Keyset... keysets) { return new HashMap<>(Arrays.stream(keysets).collect(Collectors.toMap(Keyset::getKeysetId, s -> s))); } @@ -1188,7 +1263,8 @@ void v3IdentityMapMissingValidInputKeys(String inputPayload, Vertx vertx, VertxT } @ParameterizedTest - @ValueSource(strings = {"{\"invalid_key\": []}", + @ValueSource(strings = { + "{\"invalid_key\": []}", "{\"email\": [ null ]}", "{\"email\": [ \"some_email\", null ]}" }) @@ -1271,24 +1347,11 @@ void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) @ParameterizedTest @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void v3IdentityMapWithV4UidAndNoPrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { + void v3IdentityMapWithV4UidAndV4PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); - - var lastUpdated = Instant.now().minus(1, DAYS); - var refreshFrom = lastUpdated.plus(30, DAYS); - SaltEntry salt = new SaltEntry( - 1, - "1", - lastUpdated.toEpochMilli(), - null, - refreshFrom.toEpochMilli(), - null, - new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), - null); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); JsonObject request = new JsonObject(""" { @@ -1306,8 +1369,8 @@ void v3IdentityMapWithV4UidAndNoPrevUid(String contentType, Vertx vertx, VertxTe try { var mappedEmailExpected = JsonObject.of( "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKey())), - "p", null, - "r", refreshFrom.getEpochSecond() + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousKey())), + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() ); assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); } catch (Exception e) { @@ -1329,21 +1392,8 @@ void v3IdentityMapWithV4UidAndNoPrevUid(String contentType, Vertx vertx, VertxTe void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); - - var lastUpdated = Instant.now().minus(1, DAYS); - var refreshFrom = lastUpdated.plus(30, DAYS); - SaltEntry salt = new SaltEntry( - 1, - "1", - lastUpdated.toEpochMilli(), - null, - refreshFrom.toEpochMilli(), - "salt123", - new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), - null); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + SaltEntry salt = setupSaltForV4UidAndV3PrevUid(); JsonObject request = new JsonObject(""" { @@ -1362,7 +1412,7 @@ void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTe var mappedEmailExpected = JsonObject.of( "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKey())), "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousSalt())), - "r", refreshFrom.getEpochSecond() + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() ); assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); } catch (Exception e) { @@ -1381,24 +1431,11 @@ void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTe @ParameterizedTest @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void v3IdentityMapWithV4UidAndV4PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { + void v3IdentityMapWithV4UidAndNoPrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); - - var lastUpdated = Instant.now().minus(1, DAYS); - var refreshFrom = lastUpdated.plus(30, DAYS); - SaltEntry salt = new SaltEntry( - 1, - "1", - lastUpdated.toEpochMilli(), - null, - refreshFrom.toEpochMilli(), - null, - new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), - new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + SaltEntry salt = setupSaltForV4UidAndNoPrevUid(); JsonObject request = new JsonObject(""" { @@ -1416,8 +1453,8 @@ void v3IdentityMapWithV4UidAndV4PrevUid(String contentType, Vertx vertx, VertxTe try { var mappedEmailExpected = JsonObject.of( "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKey())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousKey())), - "r", refreshFrom.getEpochSecond() + "p", null, + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() ); assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); } catch (Exception e) { @@ -1694,22 +1731,9 @@ void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestCo final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; fakeAuth(clientSiteId, Role.GENERATOR); - setupSalts(); setupKeys(); - var lastUpdated = Instant.now().minus(1, DAYS); - var refreshFrom = lastUpdated.plus(30, DAYS); - - SaltEntry salt = new SaltEntry( - 1, - "1", - lastUpdated.toEpochMilli(), - null, - refreshFrom.toEpochMilli(), - null, - new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), - new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -2003,6 +2027,35 @@ void tokenGenerateThenValidateWithEmail_Match(Vertx vertx, VertxTestContext test }); } + @Test + void tokenGenerateThenValidateWithEmailAndV4Uid_Match(Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + final String emailAddress = ValidateIdentityForEmail; + fakeAuth(clientSiteId, Role.GENERATOR); + setupKeys(); + + setupSaltForV4UidAndV4PrevUid(); + + generateTokens(vertx, "email", emailAddress, genRespJson -> { + assertEquals("success", genRespJson.getString("status")); + JsonObject genBody = genRespJson.getJsonObject("body"); + assertNotNull(genBody); + + String advertisingTokenString = genBody.getString("advertising_token"); + + JsonObject v2Payload = new JsonObject(); + v2Payload.put("token", advertisingTokenString); + v2Payload.put("email", emailAddress); + + send(vertx, "v2/token/validate", v2Payload, 200, json -> { + assertTrue(json.getBoolean("body")); + assertEquals("success", json.getString("status")); + + testContext.completeNow(); + }); + }); + } + @Test void tokenGenerateThenValidateWithEmailHash_Match(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; @@ -2522,6 +2575,21 @@ void identityMapBatchEmails(Vertx vertx, VertxTestContext testContext) { }); } + @Test + void identityMapBatchEmailsWithV4Uid(Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + fakeAuth(clientSiteId, Role.MAPPER); + + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + + JsonObject req = createBatchEmailsRequestPayload(); + + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponseWithV4Uid(json, salt, "test1@uid2.com", "test2@uid2.com"); + testContext.completeNow(); + }); + } + @Test void identityMapBatchEmailHashes(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; From 8acb47a8a8d0ac2b9e76c5722b161c74f39ad190 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Wed, 3 Sep 2025 10:07:03 +0800 Subject: [PATCH 11/38] Added /v2/token/client-generate tests --- .../operator/UIDOperatorVerticleTest.java | 264 ++++++++++++++++-- 1 file changed, 242 insertions(+), 22 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 3567a3e97..f03108888 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -1727,14 +1727,13 @@ void tokenGenerateOptOutTokenWithDisableOptoutTokenFF(String policyParameterKey, @ParameterizedTest @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { + void tokenGenerateForEmail(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; fakeAuth(clientSiteId, Role.GENERATOR); + setupSalts(); setupKeys(); - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); - JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -1750,11 +1749,7 @@ void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestCo assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKey()), advertisingToken.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); RefreshToken refreshToken = decodeRefreshToken(encoder, body.getString("decrypted_refresh_token")); assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); @@ -1773,13 +1768,14 @@ void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestCo @ParameterizedTest @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateForEmail(String contentType, Vertx vertx, VertxTestContext testContext) { + void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; fakeAuth(clientSiteId, Role.GENERATOR); - setupSalts(); setupKeys(); + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -1795,7 +1791,11 @@ void tokenGenerateForEmail(String contentType, Vertx vertx, VertxTestContext tes assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKey()), advertisingToken.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } RefreshToken refreshToken = decodeRefreshToken(encoder, body.getString("decrypted_refresh_token")); assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); @@ -2034,7 +2034,7 @@ void tokenGenerateThenValidateWithEmailAndV4Uid_Match(Vertx vertx, VertxTestCont fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - setupSaltForV4UidAndV4PrevUid(); + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); generateTokens(vertx, "email", emailAddress, genRespJson -> { assertEquals("success", genRespJson.getString("status")); @@ -2042,6 +2042,18 @@ void tokenGenerateThenValidateWithEmailAndV4Uid_Match(Vertx vertx, VertxTestCont assertNotNull(genBody); String advertisingTokenString = genBody.getString("advertising_token"); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, IdentityType.Email); + + assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); + assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); + assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKey()), advertisingToken.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } JsonObject v2Payload = new JsonObject(); v2Payload.put("token", advertisingTokenString); @@ -4222,7 +4234,6 @@ private Tuple.Tuple2 createClientSideTokenGenerateRequest } private Tuple.Tuple2 createClientSideTokenGenerateRequest(IdentityType identityType, String rawId, long timestamp, String appName) throws NoSuchAlgorithmException, InvalidKeyException { - JsonObject identity = new JsonObject(); if (identityType == IdentityType.Email) { @@ -4230,7 +4241,7 @@ private Tuple.Tuple2 createClientSideTokenGenerateRequest } else if (identityType == IdentityType.Phone) { identity.put("phone_hash", getSha256(rawId)); } else { //can't be other types - assertFalse(true); + org.junit.jupiter.api.Assertions.fail("Identity type is not: [email_hash,phone_hash]"); } return createClientSideTokenGenerateRequestWithPayload(identity, timestamp, appName); @@ -4322,7 +4333,6 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id 200, testContext, respJson -> { - if (optOutExpected) { assertEquals("optout", respJson.getString("status")); testContext.completeNow(); @@ -4381,11 +4391,207 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id }); } + @ParameterizedTest + @CsvSource({ + "true,abc@abc.com,Email", + "true,+61400000000,Phone", + "false,abc@abc.com,Email", + "false,+61400000000,Phone" + }) + void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean optOutExpected, String id, IdentityType identityType, + Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { + setupCstgBackend("cstg.co.uk"); + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + + Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); + + if (optOutExpected) { + when(optOutStore.getLatestEntry(any(UserIdentity.class))) + .thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); + } else { //not expectedOptedOut + when(optOutStore.getLatestEntry(any(UserIdentity.class))) + .thenReturn(null); + } + + sendCstg(vertx, + "v2/token/client-generate", + "https://cstg.co.uk", + data.getItem1(), + data.getItem2(), + 200, + testContext, + respJson -> { + if (optOutExpected) { + assertEquals("optout", respJson.getString("status")); + testContext.completeNow(); + return; + } + + JsonObject genBody = respJson.getJsonObject("body"); + assertNotNull(genBody); + + decodeV2RefreshToken(respJson); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKey()), advertisingToken.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); + + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt.currentKey(), false); + assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("identity_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_from")), 10); + + assertTokenStatusMetrics( + clientSideTokenGenerateSiteId, + TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.HasOriginHeader); + + String genRefreshToken = genBody.getString("refresh_token"); + //test a subsequent refresh from this cstg call and see if it still works + sendTokenRefresh(vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> { + assertEquals("success", refreshRespJson.getString("status")); + JsonObject refreshBody = refreshRespJson.getJsonObject("body"); + assertNotNull(refreshBody); + EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + //make sure the new advertising token from refresh looks right + AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder2, refreshBody, identityType); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKey()), adTokenFromRefresh.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); + assertNotEquals(genRefreshToken, refreshTokenStringNew); + RefreshToken refreshTokenAfterRefresh = decodeRefreshToken(encoder, refreshTokenStringNew, identityType); + + assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id, salt.currentKey(), false); + assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); + + assertTokenStatusMetrics( + clientSideTokenGenerateSiteId, + TokenResponseStatsCollector.Endpoint.RefreshV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.Other); + + testContext.completeNow(); + }); + }); + } + + @ParameterizedTest + @CsvSource({ + "true,abc@abc.com,Email", + "true,+61400000000,Phone", + "false,abc@abc.com,Email", + "false,+61400000000,Phone" + }) + void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean optOutExpected, String id, IdentityType identityType, + Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { + setupCstgBackend("cstg.co.uk"); + + Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); + + if (optOutExpected) { + when(optOutStore.getLatestEntry(any(UserIdentity.class))) + .thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); + } else { //not expectedOptedOut + when(optOutStore.getLatestEntry(any(UserIdentity.class))) + .thenReturn(null); + } + + sendCstg(vertx, + "v2/token/client-generate", + "https://cstg.co.uk", + data.getItem1(), + data.getItem2(), + 200, + testContext, + respJson -> { + if (optOutExpected) { + assertEquals("optout", respJson.getString("status")); + testContext.completeNow(); + return; + } + + JsonObject genBody = respJson.getJsonObject("body"); + assertNotNull(genBody); + + decodeV2RefreshToken(respJson); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); + + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); + assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("identity_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_from")), 10); + + assertTokenStatusMetrics( + clientSideTokenGenerateSiteId, + TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.HasOriginHeader); + + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + String genRefreshToken = genBody.getString("refresh_token"); + //test a subsequent refresh from this cstg call and see if it still works + sendTokenRefresh(vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> { + assertEquals("success", refreshRespJson.getString("status")); + JsonObject refreshBody = refreshRespJson.getJsonObject("body"); + assertNotNull(refreshBody); + EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + //make sure the new advertising token from refresh looks right + AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder2, refreshBody, identityType); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKey()), adTokenFromRefresh.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); + assertNotEquals(genRefreshToken, refreshTokenStringNew); + RefreshToken refreshTokenAfterRefresh = decodeRefreshToken(encoder, refreshTokenStringNew, identityType); + + assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id, salt.currentKey(), false); + assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); + assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); + + assertTokenStatusMetrics( + clientSideTokenGenerateSiteId, + TokenResponseStatsCollector.Endpoint.RefreshV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.Other); + + testContext.completeNow(); + }); + }); + } + @ParameterizedTest @CsvSource({ "https://cstg.co.uk", "https://cstg2.com", - "http://localhost:8080", + "http://localhost:8080" }) void cstgSaltsExpired(String httpOrigin, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { when(saltProviderSnapshot.getExpires()).thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); @@ -4434,7 +4640,7 @@ void cstgNoActiveKey(Vertx vertx, VertxTestContext testContext) throws NoSuchAlg @ParameterizedTest @CsvSource({ "email_hash,random@unifiedid.com", - "phone_hash,1234567890", + "phone_hash,1234567890" }) void cstgInvalidInput(String identityType, String rawUID, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); @@ -4465,17 +4671,31 @@ private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToke siteId, identityType, identity, + null, false); } - private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToken, RefreshToken refreshToken, int siteId, IdentityType identityType, String identity, boolean expectedOptOut) { + private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToken, RefreshToken refreshToken, int siteId, IdentityType identityType, String identity, SaltEntry.KeyMaterial key, boolean expectedOptOut) { final PrivacyBits advertisingTokenPrivacyBits = PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits); final PrivacyBits refreshTokenPrivacyBits = PrivacyBits.fromInt(refreshToken.userIdentity.privacyBits); - final byte[] advertisingId = getAdvertisingIdFromIdentity(identityType, - identity, - firstLevelSalt, - rotatingSalt123.currentSalt()); + final byte[] advertisingId; + if (key == null) { + advertisingId = getAdvertisingIdFromIdentity(identityType, + identity, + firstLevelSalt, + rotatingSalt123.currentSalt()); + } else { + try { + advertisingId = getAdvertisingIdFromIdentity(identityType, + identity, + firstLevelSalt, + key); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + return; + } + } final byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity(identity, firstLevelSalt); From 317ad6192b83fbb3b6b50927306ebea4b6d8eb76 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Wed, 3 Sep 2025 10:24:43 +0800 Subject: [PATCH 12/38] Added /v2/token/refresh tests --- .../operator/UIDOperatorVerticleTest.java | 160 +++++++++++++++++- 1 file changed, 151 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index f03108888..362f5bb76 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -1909,6 +1909,147 @@ void tokenGenerateThenRefresh(String contentType, Vertx vertx, VertxTestContext }, additionalHeaders); } + @ParameterizedTest + @ValueSource(strings = {"text/plain", "application/octet-stream"}) + void tokenGenerateThenRefreshWithV4UidAndRefreshedV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + final String emailAddress = "test@uid2.com"; + fakeAuth(clientSiteId, Role.GENERATOR); + setupKeys(); + + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, + HttpHeaders.CONTENT_TYPE.toString(), contentType); + + generateTokens(vertx, "email", emailAddress, genRespJson -> { + assertEquals("success", genRespJson.getString("status")); + JsonObject bodyJson = genRespJson.getJsonObject("body"); + assertNotNull(bodyJson); + + AdvertisingToken advertisingToken = validateAndGetToken(encoder, bodyJson, IdentityType.Email); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKey()), advertisingToken.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + String genRefreshToken = bodyJson.getString("refresh_token"); + + when(this.optOutStore.getLatestEntry(any())).thenReturn(null); + + sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { + assertEquals("success", refreshRespJson.getString("status")); + JsonObject refreshBody = refreshRespJson.getJsonObject("body"); + assertNotNull(refreshBody); + + AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, IdentityType.Email); + + assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenGenerated()); + assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenOptedOut()); + assertEquals(clientSiteId, adTokenFromRefresh.publisherIdentity.siteId); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKey()), adTokenFromRefresh.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); + assertNotEquals(genRefreshToken, refreshTokenStringNew); + RefreshToken refreshToken = decodeRefreshToken(encoder, refreshTokenStringNew); + assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); + assertArrayEquals(TokenUtils.getFirstLevelHashFromIdentity(emailAddress, firstLevelSalt), refreshToken.userIdentity.id); + + assertEqualsClose(now.plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); + assertEqualsClose(now.plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); + assertEqualsClose(now.plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); + + assertTokenStatusMetrics( + clientSiteId, + TokenResponseStatsCollector.Endpoint.GenerateV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.InApp); + assertTokenStatusMetrics( + clientSiteId, + TokenResponseStatsCollector.Endpoint.RefreshV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.InApp); + + testContext.completeNow(); + }, additionalHeaders); + }, additionalHeaders); + } + + @ParameterizedTest + @ValueSource(strings = {"text/plain", "application/octet-stream"}) + void tokenGenerateThenRefreshWithV3UidAndRefreshedV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { + final int clientSiteId = 201; + final String emailAddress = "test@uid2.com"; + fakeAuth(clientSiteId, Role.GENERATOR); + setupKeys(); + setupSalts(); + + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); + + Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, + HttpHeaders.CONTENT_TYPE.toString(), contentType); + + generateTokens(vertx, "email", emailAddress, genRespJson -> { + assertEquals("success", genRespJson.getString("status")); + JsonObject bodyJson = genRespJson.getJsonObject("body"); + assertNotNull(bodyJson); + + AdvertisingToken advertisingToken = validateAndGetToken(encoder, bodyJson, IdentityType.Email); + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); + + String genRefreshToken = bodyJson.getString("refresh_token"); + + when(this.optOutStore.getLatestEntry(any())).thenReturn(null); + + SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { + assertEquals("success", refreshRespJson.getString("status")); + JsonObject refreshBody = refreshRespJson.getJsonObject("body"); + assertNotNull(refreshBody); + + AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, IdentityType.Email); + + assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenGenerated()); + assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenOptedOut()); + assertEquals(clientSiteId, adTokenFromRefresh.publisherIdentity.siteId); + try { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKey()), adTokenFromRefresh.userIdentity.id); + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + } + + String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); + assertNotEquals(genRefreshToken, refreshTokenStringNew); + RefreshToken refreshToken = decodeRefreshToken(encoder, refreshTokenStringNew); + assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); + assertArrayEquals(TokenUtils.getFirstLevelHashFromIdentity(emailAddress, firstLevelSalt), refreshToken.userIdentity.id); + + assertEqualsClose(now.plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); + assertEqualsClose(now.plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); + assertEqualsClose(now.plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); + + assertTokenStatusMetrics( + clientSiteId, + TokenResponseStatsCollector.Endpoint.GenerateV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.InApp); + assertTokenStatusMetrics( + clientSiteId, + TokenResponseStatsCollector.Endpoint.RefreshV2, + TokenResponseStatsCollector.ResponseStatus.Success, + TokenResponseStatsCollector.PlatformType.InApp); + + testContext.completeNow(); + }, additionalHeaders); + }, additionalHeaders); + } + @Test void tokenGenerateThenRefreshSaltsExpired(Vertx vertx, VertxTestContext testContext) { when(saltProviderSnapshot.getExpires()).thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); @@ -4314,6 +4455,7 @@ void cstgUserOptsOutAfterTokenGenerate(String id, IdentityType identityType, Ver void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id, IdentityType identityType, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); @@ -4343,7 +4485,6 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id assertNotNull(genBody); decodeV2RefreshToken(respJson); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); @@ -4366,10 +4507,9 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); //make sure the new advertising token from refresh looks right - AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder2, refreshBody, identityType); + AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); assertNotEquals(genRefreshToken, refreshTokenStringNew); @@ -4402,6 +4542,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); @@ -4431,7 +4572,6 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean assertNotNull(genBody); decodeV2RefreshToken(respJson); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); try { @@ -4459,10 +4599,9 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); //make sure the new advertising token from refresh looks right - AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder2, refreshBody, identityType); + AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); try { assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKey()), adTokenFromRefresh.userIdentity.id); } catch (Exception e) { @@ -4499,6 +4638,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean optOutExpected, String id, IdentityType identityType, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); @@ -4528,7 +4668,6 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean assertNotNull(genBody); decodeV2RefreshToken(respJson); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); try { @@ -4557,10 +4696,9 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder2 = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); //make sure the new advertising token from refresh looks right - AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder2, refreshBody, identityType); + AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); try { assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKey()), adTokenFromRefresh.userIdentity.id); } catch (Exception e) { @@ -4596,7 +4734,9 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean void cstgSaltsExpired(String httpOrigin, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { when(saltProviderSnapshot.getExpires()).thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); setupCstgBackend("cstg.co.uk", "cstg2.com", "localhost"); + Tuple.Tuple2 data = createClientSideTokenGenerateRequest(IdentityType.Email, "random@unifiedid.com", Instant.now().toEpochMilli()); + sendCstg(vertx, "v2/token/client-generate", httpOrigin, @@ -4622,7 +4762,9 @@ void cstgSaltsExpired(String httpOrigin, Vertx vertx, VertxTestContext testConte void cstgNoActiveKey(Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); setupKeys(true); + Tuple.Tuple2 data = createClientSideTokenGenerateRequest(IdentityType.Email, "random@unifiedid.com", Instant.now().toEpochMilli()); + sendCstg(vertx, "v2/token/client-generate", "http://cstg.co.uk", From 23bd981d11dad3a64b1ff9ae1138af505dbac6d5 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Wed, 3 Sep 2025 16:22:25 +0800 Subject: [PATCH 13/38] Combined tests --- .../com/uid2/operator/service/TokenUtils.java | 8 +- .../operator/UIDOperatorVerticleTest.java | 965 +++++++----------- 2 files changed, 349 insertions(+), 624 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index 0bd89e284..556114061 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -63,8 +63,12 @@ public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), encryptingKey.salt()); } - public static byte[] getAdvertisingIdV4FromIdentity(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identity, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) throws Exception { - return getAdvertisingIdV4(scope, type, environment, getFirstLevelHashFromIdentity(identity, firstLevelSalt), encryptingKey); + public static byte[] getAdvertisingIdV4FromIdentity(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) throws Exception { + return getAdvertisingIdV4(scope, type, environment, getFirstLevelHashFromIdentity(identityString, firstLevelSalt), encryptingKey); + } + + public static byte[] getAdvertisingIdV4FromIdentityHash(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) throws Exception { + return getAdvertisingIdV4(scope, type, environment, getFirstLevelHashFromIdentityHash(identityString, firstLevelSalt), encryptingKey); } public static byte encodeIdentityScope(IdentityScope identityScope) { diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 059637fb9..63dfc1200 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -529,12 +529,13 @@ private void checkIdentityMapResponseWithV4Uid(JsonObject response, SaltEntry sa } } - protected void setupSalts() { + protected SaltEntry setupSalts() { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(rotatingSalt123); + return rotatingSalt123; } - protected SaltEntry setupSaltForV4UidAndV4PrevUid() { + protected SaltEntry setupSaltsForV4UidAndV4PrevUid() { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); var lastUpdated = Instant.now().minus(1, DAYS); @@ -552,7 +553,7 @@ protected SaltEntry setupSaltForV4UidAndV4PrevUid() { return salt; } - protected SaltEntry setupSaltForV4UidAndV3PrevUid() { + protected SaltEntry setupSaltsForV4UidAndV3PrevUid() { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); var lastUpdated = Instant.now().minus(1, DAYS); @@ -570,7 +571,7 @@ protected SaltEntry setupSaltForV4UidAndV3PrevUid() { return salt; } - protected SaltEntry setupSaltForV4UidAndNoPrevUid() { + protected SaltEntry setupSaltsForV4UidAndNoPrevUid() { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); var lastUpdated = Instant.now().minus(1, DAYS); @@ -725,6 +726,10 @@ private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, Strin : TokenUtils.getAdvertisingIdV3FromIdentityHash(getIdentityScope(), identityType, identityString, firstLevelSalt, rotatingSalt); } + private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) throws Exception { + return TokenUtils.getAdvertisingIdV4FromIdentityHash(getIdentityScope(), identityType, IdentityEnvironment.Test, identityString, firstLevelSalt, rotatingKey); + } + private JsonObject createBatchEmailsRequestPayload() { JsonArray emails = new JsonArray(); emails.add("test1@uid2.com"); @@ -1118,16 +1123,65 @@ void identityMapNewClientWrongPolicySpecifiedOlderKeySuccessful(String policyPar } @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void v3IdentityMapMixedInputSuccess(String contentType, Vertx vertx, VertxTestContext testContext) { + @CsvSource(value = { + // After - V4 UID, V4 previous UID + "true,true,text/plain", + "true,true,application/octet-stream", + + // Migration - V4 UID, V3 previous UID + "true,false,text/plain", + "true,false,application/octet-stream", + + // V4 UID, no previous UID + "true,,text/plain", + "true,,application/octet-stream", + + // Rollback - V3 UID, V4 previous UID + "false,true,text/plain", + "false,true,application/octet-stream", + + // Before - V3 UID, V3 previous UID + "false,false,text/plain", + "false,false,application/octet-stream", + + // V3 UID, no previous UID + "false,,text/plain", + "false,,application/octet-stream" + }) + void v3IdentityMapMixedInputSuccess( + boolean useV4Uid, Boolean useV4PrevUid, String contentType, + Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); - var lastUpdated = Instant.now().minus(1, DAYS); - var refreshFrom = lastUpdated.plus(30, DAYS); - - SaltEntry salt = new SaltEntry(1, "1", lastUpdated.toEpochMilli(), "salt", refreshFrom.toEpochMilli(), "previousSalt", null, null); + SaltEntry salt; + if (useV4Uid) { + if (useV4PrevUid == null) { + salt = setupSaltsForV4UidAndNoPrevUid(); + } else if (useV4PrevUid) { + salt = setupSaltsForV4UidAndV4PrevUid(); + } else { + salt = setupSaltsForV4UidAndV3PrevUid(); + } + } else { + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + salt = setupSalts(); + + if (useV4PrevUid == null) { + salt = new SaltEntry( + salt.id(), salt.hashedId(), lastUpdated.toEpochMilli(), + salt.currentSalt(), refreshFrom.toEpochMilli(), null, null, null); + } else if (useV4PrevUid) { + salt = new SaltEntry( + salt.id(), salt.hashedId(), lastUpdated.toEpochMilli(), + salt.currentSalt(), refreshFrom.toEpochMilli(), null, null, new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); + } else { + salt = new SaltEntry( + salt.id(), salt.hashedId(), lastUpdated.toEpochMilli(), + salt.currentSalt(), refreshFrom.toEpochMilli(), salt.previousSalt(), null, null); + } + } when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); var phoneHash = TokenUtils.getIdentityHashString("+15555555555"); @@ -1140,40 +1194,146 @@ void v3IdentityMapMixedInputSuccess(String contentType, Vertx vertx, VertxTestCo """, phoneHash) ); + SaltEntry finalSalt = salt; send(vertx, "v3/identity/map", request, 200, respJson -> { JsonObject body = respJson.getJsonObject("body"); assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); var mappedEmails = body.getJsonArray("email"); assertEquals(2, mappedEmails.size()); + JsonObject mappedEmailExpected1; + JsonObject mappedEmailExpected2; - var mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousSalt())), - "r", refreshFrom.getEpochSecond() - ); - assertEquals(mappedEmailExpected1, mappedEmails.getJsonObject(0)); + var mappedPhoneHash = body.getJsonArray("phone_hash"); + assertEquals(1, mappedPhoneHash.size()); + JsonObject mappedPhoneHashExpected; - var mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt.previousSalt())), - "r", refreshFrom.getEpochSecond() - ); + try { + if (useV4Uid) { + if (useV4PrevUid == null) { + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), + "p", null, + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), + "p", null, + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentKeySalt())), + "p", null, + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + } else if (useV4PrevUid) { + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentKeySalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousKeySalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + } else { + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousSalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousSalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentKeySalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousSalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + } + } else { + if (useV4PrevUid == null) { + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentSalt())), + "p", null, + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentSalt())), + "p", null, + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentSalt())), + "p", null, + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + } else if (useV4PrevUid) { + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousKeySalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + } else { + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousSalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousSalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentSalt())), + "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousSalt())), + "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() + ); + } + } + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + testContext.failNow(e); + return; + } + assertEquals(mappedEmailExpected1, mappedEmails.getJsonObject(0)); assertEquals(mappedEmailExpected2, mappedEmails.getJsonObject(1)); + assertEquals(mappedPhoneHashExpected, mappedPhoneHash.getJsonObject(0)); assertEquals(0, body.getJsonArray("email_hash").size()); assertEquals(0, body.getJsonArray("phone").size()); - var mappedPhoneHash = body.getJsonArray("phone_hash"); - assertEquals(1, mappedPhoneHash.size()); - - var mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt.previousSalt())), - "r", refreshFrom.getEpochSecond() - ); - assertEquals(mappedPhoneHashExpected, mappedPhoneHash.getJsonObject(0)); - assertEquals("success", respJson.getString("status")); testContext.completeNow(); }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); @@ -1345,132 +1505,6 @@ void v3IdentityMapOutdatedRefreshFrom(Vertx vertx, VertxTestContext testContext) }); } - @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void v3IdentityMapWithV4UidAndV4PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - fakeAuth(clientSiteId, Role.MAPPER); - - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); - - JsonObject request = new JsonObject(""" - { - "email": ["test1@uid2.com"] - } - """); - - send(vertx, "v3/identity/map", request, 200, respJson -> { - JsonObject body = respJson.getJsonObject("body"); - assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); - - var mappedEmails = body.getJsonArray("email"); - assertEquals(1, mappedEmails.size()); - - try { - var mappedEmailExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousKeySalt())), - "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() - ); - assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - } - - assertEquals(0, body.getJsonArray("email_hash").size()); - assertEquals(0, body.getJsonArray("phone").size()); - var mappedPhoneHash = body.getJsonArray("phone_hash"); - assertEquals(0, mappedPhoneHash.size()); - - assertEquals("success", respJson.getString("status")); - testContext.completeNow(); - }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); - } - - @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void v3IdentityMapWithV4UidAndV3PrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - fakeAuth(clientSiteId, Role.MAPPER); - - SaltEntry salt = setupSaltForV4UidAndV3PrevUid(); - - JsonObject request = new JsonObject(""" - { - "email": ["test1@uid2.com"] - } - """); - - send(vertx, "v3/identity/map", request, 200, respJson -> { - JsonObject body = respJson.getJsonObject("body"); - assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); - - var mappedEmails = body.getJsonArray("email"); - assertEquals(1, mappedEmails.size()); - - try { - var mappedEmailExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.previousSalt())), - "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() - ); - assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - } - - assertEquals(0, body.getJsonArray("email_hash").size()); - assertEquals(0, body.getJsonArray("phone").size()); - var mappedPhoneHash = body.getJsonArray("phone_hash"); - assertEquals(0, mappedPhoneHash.size()); - - assertEquals("success", respJson.getString("status")); - testContext.completeNow(); - }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); - } - - @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void v3IdentityMapWithV4UidAndNoPrevUid(String contentType, Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - fakeAuth(clientSiteId, Role.MAPPER); - - SaltEntry salt = setupSaltForV4UidAndNoPrevUid(); - - JsonObject request = new JsonObject(""" - { - "email": ["test1@uid2.com"] - } - """); - - send(vertx, "v3/identity/map", request, 200, respJson -> { - JsonObject body = respJson.getJsonObject("body"); - assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); - - var mappedEmails = body.getJsonArray("email"); - assertEquals(1, mappedEmails.size()); - - try { - var mappedEmailExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt.currentKeySalt())), - "p", null, - "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() - ); - assertEquals(mappedEmailExpected, mappedEmails.getJsonObject(0)); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - } - - assertEquals(0, body.getJsonArray("email_hash").size()); - assertEquals(0, body.getJsonArray("phone").size()); - var mappedPhoneHash = body.getJsonArray("phone_hash"); - assertEquals(0, mappedPhoneHash.size()); - - assertEquals("success", respJson.getString("status")); - testContext.completeNow(); - }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); - } - @Test void tokenGenerateNewClientNoPolicySpecified(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; @@ -1726,55 +1760,21 @@ void tokenGenerateOptOutTokenWithDisableOptoutTokenFF(String policyParameterKey, } @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateForEmail(String contentType, Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - final String emailAddress = "test@uid2.com"; - fakeAuth(clientSiteId, Role.GENERATOR); - setupSalts(); - setupKeys(); + @CsvSource(value = { + "true,text/plain", + "true,application/octet-stream", - JsonObject v2Payload = new JsonObject(); - v2Payload.put("email", emailAddress); - - sendTokenGenerate(vertx, v2Payload, 200, - json -> { - assertEquals("success", json.getString("status")); - JsonObject body = json.getJsonObject("body"); - assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - - AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); - - assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); - assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); - assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); - - RefreshToken refreshToken = decodeRefreshToken(encoder, body.getString("decrypted_refresh_token")); - assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); - assertArrayEquals(TokenUtils.getFirstLevelHashFromIdentity(emailAddress, firstLevelSalt), refreshToken.userIdentity.id); - - assertEqualsClose(now.plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(body.getLong("identity_expires")), 10); - assertEqualsClose(now.plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(body.getLong("refresh_expires")), 10); - assertEqualsClose(now.plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(body.getLong("refresh_from")), 10); - - assertStatsCollector("/v2/token/generate", null, "test-contact", clientSiteId); - - testContext.completeNow(); - }, - Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); - } - - @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { + "false,text/plain", + "false,application/octet-stream" + }) + void tokenGenerateForEmail(boolean useV4Uid, String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -1784,7 +1784,6 @@ void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestCo assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -1792,9 +1791,15 @@ void tokenGenerateForEmailWithV4Uid(String contentType, Vertx vertx, VertxTestCo assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); + if (useV4Uid) { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); + } else { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); + } } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); + testContext.failNow(e); + return; } RefreshToken refreshToken = decodeRefreshToken(encoder, body.getString("decrypted_refresh_token")); @@ -1850,74 +1855,32 @@ void tokenGenerateForEmailHash(Vertx vertx, VertxTestContext testContext) { } @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateThenRefresh(String contentType, Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - final String emailAddress = "test@uid2.com"; - fakeAuth(clientSiteId, Role.GENERATOR); - setupSalts(); - setupKeys(); - - Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, - HttpHeaders.CONTENT_TYPE.toString(), contentType); - - generateTokens(vertx, "email", emailAddress, genRespJson -> { - assertEquals("success", genRespJson.getString("status")); - JsonObject bodyJson = genRespJson.getJsonObject("body"); - assertNotNull(bodyJson); - - String genRefreshToken = bodyJson.getString("refresh_token"); - - when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - - sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { - assertEquals("success", refreshRespJson.getString("status")); - JsonObject refreshBody = refreshRespJson.getJsonObject("body"); - assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - - AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Email); - - assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); - assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); - assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); - - String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); - assertNotEquals(genRefreshToken, refreshTokenStringNew); - RefreshToken refreshToken = decodeRefreshToken(encoder, refreshTokenStringNew); - assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); - assertArrayEquals(TokenUtils.getFirstLevelHashFromIdentity(emailAddress, firstLevelSalt), refreshToken.userIdentity.id); - - assertEqualsClose(now.plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); - assertEqualsClose(now.plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); - assertEqualsClose(now.plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); - - assertTokenStatusMetrics( - clientSiteId, - TokenResponseStatsCollector.Endpoint.GenerateV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.InApp); - assertTokenStatusMetrics( - clientSiteId, - TokenResponseStatsCollector.Endpoint.RefreshV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.InApp); - - testContext.completeNow(); - }, additionalHeaders); - }, additionalHeaders); - } - - @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateThenRefreshWithV4UidAndRefreshedV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { + @CsvSource(value = { + // Before - v4 UID, v4 refreshed UID + "true,true,text/plain", + "true,true,application/octet-stream", + + // Rollback - v4 UID, v3 refreshed UID + "true,false,text/plain", + "true,false,application/octet-stream", + + // Migration - v3 UID, v4 refreshed UID + "false,true,text/plain", + "false,true,application/octet-stream", + + // After - v3 UID, v3 refreshed UID + "false,false,text/plain", + "false,false,application/octet-stream" + }) + void tokenGenerateThenRefresh( + boolean useV4Uid, boolean useRefreshedV4Uid, String contentType, + Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, @@ -1930,84 +1893,22 @@ void tokenGenerateThenRefreshWithV4UidAndRefreshedV4Uid(String contentType, Vert AdvertisingToken advertisingToken = validateAndGetToken(encoder, bodyJson, IdentityType.Email); try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - } - - String genRefreshToken = bodyJson.getString("refresh_token"); - - when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - - sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { - assertEquals("success", refreshRespJson.getString("status")); - JsonObject refreshBody = refreshRespJson.getJsonObject("body"); - assertNotNull(refreshBody); - - AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, IdentityType.Email); - - assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenGenerated()); - assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenOptedOut()); - assertEquals(clientSiteId, adTokenFromRefresh.publisherIdentity.siteId); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - } - - String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); - assertNotEquals(genRefreshToken, refreshTokenStringNew); - RefreshToken refreshToken = decodeRefreshToken(encoder, refreshTokenStringNew); - assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); - assertArrayEquals(TokenUtils.getFirstLevelHashFromIdentity(emailAddress, firstLevelSalt), refreshToken.userIdentity.id); - - assertEqualsClose(now.plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); - assertEqualsClose(now.plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); - assertEqualsClose(now.plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); - - assertTokenStatusMetrics( - clientSiteId, - TokenResponseStatsCollector.Endpoint.GenerateV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.InApp); - assertTokenStatusMetrics( - clientSiteId, - TokenResponseStatsCollector.Endpoint.RefreshV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.InApp); - - testContext.completeNow(); - }, additionalHeaders); - }, additionalHeaders); - } - - @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateThenRefreshWithV3UidAndRefreshedV4Uid(String contentType, Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - final String emailAddress = "test@uid2.com"; - fakeAuth(clientSiteId, Role.GENERATOR); - setupKeys(); - setupSalts(); - - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - - Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, - HttpHeaders.CONTENT_TYPE.toString(), contentType); - - generateTokens(vertx, "email", emailAddress, genRespJson -> { - assertEquals("success", genRespJson.getString("status")); - JsonObject bodyJson = genRespJson.getJsonObject("body"); - assertNotNull(bodyJson); - - AdvertisingToken advertisingToken = validateAndGetToken(encoder, bodyJson, IdentityType.Email); - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); + if (useV4Uid) { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); + } else { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); + } + } catch (Exception e) { + org.junit.jupiter.api.Assertions.fail(e.getMessage()); + testContext.failNow(e); + return; + } String genRefreshToken = bodyJson.getString("refresh_token"); when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + SaltEntry refreshSalt = useRefreshedV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); @@ -2019,9 +1920,15 @@ void tokenGenerateThenRefreshWithV3UidAndRefreshedV4Uid(String contentType, Vert assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, adTokenFromRefresh.publisherIdentity.siteId); try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); + if (useRefreshedV4Uid) { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); + } else { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt.currentSalt()), adTokenFromRefresh.userIdentity.id); + } } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); + testContext.failNow(e); + return; } String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); @@ -2140,42 +2047,16 @@ void tokenGenerateThenRefreshNoActiveKey(Vertx vertx, VertxTestContext testConte }); } - @Test - void tokenGenerateThenValidateWithEmail_Match(Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - final String emailAddress = ValidateIdentityForEmail; - fakeAuth(clientSiteId, Role.GENERATOR); - setupSalts(); - setupKeys(); - - generateTokens(vertx, "email", emailAddress, genRespJson -> { - assertEquals("success", genRespJson.getString("status")); - JsonObject genBody = genRespJson.getJsonObject("body"); - assertNotNull(genBody); - - String advertisingTokenString = genBody.getString("advertising_token"); - - JsonObject v2Payload = new JsonObject(); - v2Payload.put("token", advertisingTokenString); - v2Payload.put("email", emailAddress); - - send(vertx, "v2/token/validate", v2Payload, 200, json -> { - assertTrue(json.getBoolean("body")); - assertEquals("success", json.getString("status")); - - testContext.completeNow(); - }); - }); - } - - @Test - void tokenGenerateThenValidateWithEmailAndV4Uid_Match(Vertx vertx, VertxTestContext testContext) { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void tokenGenerateThenValidateWithEmail_Match(boolean useV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = ValidateIdentityForEmail; fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); generateTokens(vertx, "email", emailAddress, genRespJson -> { assertEquals("success", genRespJson.getString("status")); @@ -2183,17 +2064,17 @@ void tokenGenerateThenValidateWithEmailAndV4Uid_Match(Vertx vertx, VertxTestCont assertNotNull(genBody); String advertisingTokenString = genBody.getString("advertising_token"); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, IdentityType.Email); - - assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); - assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); - assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); + if (useV4Uid) { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); + } else { + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); + } } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); + testContext.failNow(e); + return; } JsonObject v2Payload = new JsonObject(); @@ -2577,7 +2458,6 @@ void tokenValidateWithEmailHash_Mismatch(Vertx vertx, VertxTestContext testConte }); } - @Test void identityMapBatchBothEmailAndHashEmpty(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; @@ -2713,32 +2593,23 @@ void identityMapSinglePhoneHashProvided(Vertx vertx, VertxTestContext testContex }); } - @Test - void identityMapBatchEmails(Vertx vertx, VertxTestContext testContext) { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void identityMapBatchEmails(boolean useV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); - JsonObject req = createBatchEmailsRequestPayload(); - - send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "test1@uid2.com", "test2@uid2.com"); - testContext.completeNow(); - }); - } - - @Test - void identityMapBatchEmailsWithV4Uid(Vertx vertx, VertxTestContext testContext) { - final int clientSiteId = 201; - fakeAuth(clientSiteId, Role.MAPPER); - - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); JsonObject req = createBatchEmailsRequestPayload(); send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponseWithV4Uid(json, salt, "test1@uid2.com", "test2@uid2.com"); + if (useV4Uid) { + checkIdentityMapResponseWithV4Uid(json, salt, "test1@uid2.com", "test2@uid2.com"); + } else { + checkIdentityMapResponse(json, "test1@uid2.com", "test2@uid2.com"); + } testContext.completeNow(); }); } @@ -3278,11 +3149,9 @@ void tokenGenerateThenValidateWithBothPhoneAndPhoneHash(Vertx vertx, VertxTestCo }); } - @Test void identityMapBatchBothPhoneAndHashEmpty(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3293,7 +3162,7 @@ void identityMapBatchBothPhoneAndHashEmpty(Vertx vertx, VertxTestContext testCon req.put("phone", phones); req.put("phone_hash", phoneHashes); - send(vertx, apiVersion + "/identity/map", req, 200, respJson -> { + send(vertx, "v2/identity/map", req, 200, respJson -> { checkIdentityMapResponse(respJson); testContext.completeNow(); }); @@ -3302,7 +3171,6 @@ void identityMapBatchBothPhoneAndHashEmpty(Vertx vertx, VertxTestContext testCon @Test void identityMapBatchBothPhoneAndHashSpecified(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3316,7 +3184,7 @@ void identityMapBatchBothPhoneAndHashSpecified(Vertx vertx, VertxTestContext tes phones.add("+15555555555"); phoneHashes.add(TokenUtils.getIdentityHashString("+15555555555")); - send(vertx, apiVersion + "/identity/map", req, 400, respJson -> { + send(vertx, "v2/identity/map", req, 400, respJson -> { assertFalse(respJson.containsKey("body")); assertEquals("client_error", respJson.getString("status")); testContext.completeNow(); @@ -3326,7 +3194,6 @@ void identityMapBatchBothPhoneAndHashSpecified(Vertx vertx, VertxTestContext tes @Test void identityMapBatchPhones(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3338,7 +3205,7 @@ void identityMapBatchPhones(Vertx vertx, VertxTestContext testContext) { phones.add("+15555555555"); phones.add("+15555555556"); - send(vertx, apiVersion + "/identity/map", req, 200, json -> { + send(vertx, "v2/identity/map", req, 200, json -> { checkIdentityMapResponse(json, "+15555555555", "+15555555556"); testContext.completeNow(); }); @@ -3347,7 +3214,6 @@ void identityMapBatchPhones(Vertx vertx, VertxTestContext testContext) { @Test void identityMapBatchPhoneHashes(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3355,17 +3221,17 @@ void identityMapBatchPhoneHashes(Vertx vertx, VertxTestContext testContext) { JsonObject req = new JsonObject(); JsonArray hashes = new JsonArray(); req.put("phone_hash", hashes); - final String[] email_hashes = { + final String[] emailHashes = { TokenUtils.getIdentityHashString("+15555555555"), TokenUtils.getIdentityHashString("+15555555556"), }; - for (String email_hash : email_hashes) { - hashes.add(email_hash); + for (String emailHash : emailHashes) { + hashes.add(emailHash); } - send(vertx, apiVersion + "/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, email_hashes); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json, emailHashes); testContext.completeNow(); }); } @@ -3373,7 +3239,6 @@ void identityMapBatchPhoneHashes(Vertx vertx, VertxTestContext testContext) { @Test void identityMapBatchPhonesOnePhoneInvalid(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3386,7 +3251,7 @@ void identityMapBatchPhonesOnePhoneInvalid(Vertx vertx, VertxTestContext testCon phones.add("bogus"); phones.add("+15555555556"); - send(vertx, apiVersion + "/identity/map", req, 200, json -> { + send(vertx, "v2/identity/map", req, 200, json -> { checkIdentityMapResponse(json, "+15555555555", "+15555555556"); testContext.completeNow(); }); @@ -3395,7 +3260,6 @@ void identityMapBatchPhonesOnePhoneInvalid(Vertx vertx, VertxTestContext testCon @Test void identityMapBatchPhonesNoPhones(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3404,7 +3268,7 @@ void identityMapBatchPhonesNoPhones(Vertx vertx, VertxTestContext testContext) { JsonArray phones = new JsonArray(); req.put("phone", phones); - send(vertx, apiVersion + "/identity/map", req, 200, json -> { + send(vertx, "v2/identity/map", req, 200, json -> { checkIdentityMapResponse(json); testContext.completeNow(); }); @@ -3413,7 +3277,6 @@ void identityMapBatchPhonesNoPhones(Vertx vertx, VertxTestContext testContext) { @Test void identityMapBatchRequestTooLargeForPhone(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3427,7 +3290,7 @@ void identityMapBatchRequestTooLargeForPhone(Vertx vertx, VertxTestContext testC phones.add(phone); } - send(vertx, apiVersion + "/identity/map", req, 413, json -> testContext.completeNow()); + send(vertx, "v2/identity/map", req, 413, json -> testContext.completeNow()); } @ParameterizedTest @@ -3464,7 +3327,6 @@ void tokenGenerateRespectOptOutOption(String policyParameterKey, Vertx vertx, Ve @Test void identityMapDefaultOption(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3478,7 +3340,7 @@ void identityMapDefaultOption(Vertx vertx, VertxTestContext testContext) { emails.add("random-optout-user@email.io"); req.put("email", emails); - send(vertx, apiVersion + "/identity/map", req, 200, json -> { + send(vertx, "v2/identity/map", req, 200, json -> { try { Assertions.assertTrue(json.getJsonObject("body").getJsonArray("mapped") == null || json.getJsonObject("body").getJsonArray("mapped").isEmpty()); @@ -3501,7 +3363,6 @@ private static Stream policyParameters() { @MethodSource("policyParameters") void identityMapRespectOptOutOption(String policyParameterKey, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); setupSalts(); setupKeys(); @@ -3516,7 +3377,7 @@ void identityMapRespectOptOutOption(String policyParameterKey, Vertx vertx, Vert req.put("email", emails); req.put(policyParameterKey, 1); - send(vertx, apiVersion + "/identity/map", req, 200, json -> { + send(vertx, "v2/identity/map", req, 200, json -> { try { Assertions.assertTrue(json.getJsonObject("body").getJsonArray("mapped").isEmpty()); Assertions.assertEquals(1, json.getJsonObject("body").getJsonArray("unmapped").size()); @@ -4442,106 +4303,42 @@ void cstgUserOptsOutAfterTokenGenerate(String id, IdentityType identityType, Ver } // tests for opted out user should lead to generating ad tokens with optout success response - // tests for non-opted out user should generate the UID2 identity and the generated refresh token can be - // refreshed again - // tests for all email/phone combos +// tests for non-opted out user should generate the UID2 identity and the generated refresh token can be +// refreshed again +// tests for all email/phone combos @ParameterizedTest @CsvSource({ - "true,abc@abc.com,Email", - "true,+61400000000,Phone", - "false,abc@abc.com,Email", - "false,+61400000000,Phone" + // After - v4 UID, refreshed v4 UID + "true,true,true,abc@abc.com,Email", + "true,true,true,+61400000000,Phone", + "false,true,true,abc@abc.com,Email", + "false,true,true,+61400000000,Phone", + + // Rollback - v4 UID, refreshed v3 UID + "true,true,false,abc@abc.com,Email", + "true,true,false,+61400000000,Phone", + "false,true,false,abc@abc.com,Email", + "false,true,false,+61400000000,Phone", + + // Migration - v3 UID, refreshed v4 UID + "true,false,true,abc@abc.com,Email", + "true,false,true,+61400000000,Phone", + "false,false,true,abc@abc.com,Email", + "false,false,true,+61400000000,Phone", + + // Before - v3 UID, refreshed v3 UID + "true,false,false,abc@abc.com,Email", + "true,false,false,+61400000000,Phone", + "false,false,false,abc@abc.com,Email", + "false,false,false,+61400000000,Phone" }) - void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id, IdentityType identityType, - Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { + void cstgSuccessForBothOptedAndNonOptedOutTest( + boolean optOutExpected, boolean useV4Uid, boolean useRefreshedV4Uid, + String id, IdentityType identityType, + Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - - Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); - - if (optOutExpected) { - when(optOutStore.getLatestEntry(any(UserIdentity.class))) - .thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); - } else { //not expectedOptedOut - when(optOutStore.getLatestEntry(any(UserIdentity.class))) - .thenReturn(null); - } - - sendCstg(vertx, - "v2/token/client-generate", - "https://cstg.co.uk", - data.getItem1(), - data.getItem2(), - 200, - testContext, - respJson -> { - if (optOutExpected) { - assertEquals("optout", respJson.getString("status")); - testContext.completeNow(); - return; - } - - JsonObject genBody = respJson.getJsonObject("body"); - assertNotNull(genBody); - - decodeV2RefreshToken(respJson); - - AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); - - RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); - - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); - assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("identity_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_from")), 10); - - assertTokenStatusMetrics( - clientSideTokenGenerateSiteId, - TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.HasOriginHeader); - - String genRefreshToken = genBody.getString("refresh_token"); - //test a subsequent refresh from this cstg call and see if it still works - sendTokenRefresh(vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> { - assertEquals("success", refreshRespJson.getString("status")); - JsonObject refreshBody = refreshRespJson.getJsonObject("body"); - assertNotNull(refreshBody); - - //make sure the new advertising token from refresh looks right - AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); - - String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); - assertNotEquals(genRefreshToken, refreshTokenStringNew); - RefreshToken refreshTokenAfterRefresh = decodeRefreshToken(encoder, refreshTokenStringNew, identityType); - - assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id); - assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); - - assertTokenStatusMetrics( - clientSideTokenGenerateSiteId, - TokenResponseStatsCollector.Endpoint.RefreshV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.Other); - - testContext.completeNow(); - }); - }); - } - @ParameterizedTest - @CsvSource({ - "true,abc@abc.com,Email", - "true,+61400000000,Phone", - "false,abc@abc.com,Email", - "false,+61400000000,Phone" - }) - void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean optOutExpected, String id, IdentityType identityType, - Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { - setupCstgBackend("cstg.co.uk"); - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); @@ -4549,7 +4346,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean if (optOutExpected) { when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); - } else { //not expectedOptedOut + } else { when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(null); } @@ -4575,110 +4372,24 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV4UidAndRefreshedV4Uid(boolean AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); try { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - } - - RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); - - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt.currentKeySalt(), false); - assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("identity_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_from")), 10); - - assertTokenStatusMetrics( - clientSideTokenGenerateSiteId, - TokenResponseStatsCollector.Endpoint.ClientSideTokenGenerateV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.HasOriginHeader); - - String genRefreshToken = genBody.getString("refresh_token"); - //test a subsequent refresh from this cstg call and see if it still works - sendTokenRefresh(vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> { - assertEquals("success", refreshRespJson.getString("status")); - JsonObject refreshBody = refreshRespJson.getJsonObject("body"); - assertNotNull(refreshBody); - - //make sure the new advertising token from refresh looks right - AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); + if (useV4Uid) { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); + } else { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); } - - String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); - assertNotEquals(genRefreshToken, refreshTokenStringNew); - RefreshToken refreshTokenAfterRefresh = decodeRefreshToken(encoder, refreshTokenStringNew, identityType); - - assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id, salt.currentKeySalt(), false); - assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); - assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); - - assertTokenStatusMetrics( - clientSideTokenGenerateSiteId, - TokenResponseStatsCollector.Endpoint.RefreshV2, - TokenResponseStatsCollector.ResponseStatus.Success, - TokenResponseStatsCollector.PlatformType.Other); - - testContext.completeNow(); - }); - }); - } - - @ParameterizedTest - @CsvSource({ - "true,abc@abc.com,Email", - "true,+61400000000,Phone", - "false,abc@abc.com,Email", - "false,+61400000000,Phone" - }) - void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean optOutExpected, String id, IdentityType identityType, - Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { - setupCstgBackend("cstg.co.uk"); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - - Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); - - if (optOutExpected) { - when(optOutStore.getLatestEntry(any(UserIdentity.class))) - .thenReturn(Instant.now().minus(1, ChronoUnit.HOURS)); - } else { //not expectedOptedOut - when(optOutStore.getLatestEntry(any(UserIdentity.class))) - .thenReturn(null); - } - - sendCstg(vertx, - "v2/token/client-generate", - "https://cstg.co.uk", - data.getItem1(), - data.getItem2(), - 200, - testContext, - respJson -> { - if (optOutExpected) { - assertEquals("optout", respJson.getString("status")); - testContext.completeNow(); - return; - } - - JsonObject genBody = respJson.getJsonObject("body"); - assertNotNull(genBody); - - decodeV2RefreshToken(respJson); - - AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, rotatingSalt123.currentSalt()), advertisingToken.userIdentity.id); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); + testContext.failNow(e); + return; } RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); + if (useV4Uid) { + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt.currentKeySalt(), false); + } else { + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); + } assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("identity_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_from")), 10); @@ -4689,7 +4400,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean TokenResponseStatsCollector.ResponseStatus.Success, TokenResponseStatsCollector.PlatformType.HasOriginHeader); - SaltEntry salt = setupSaltForV4UidAndV4PrevUid(); + SaltEntry refreshSalt = useRefreshedV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); String genRefreshToken = genBody.getString("refresh_token"); //test a subsequent refresh from this cstg call and see if it still works sendTokenRefresh(vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> { @@ -4700,16 +4411,26 @@ void cstgSuccessForBothOptedAndNonOptedOutTestWithV3UidAndRefreshedV4Uid(boolean //make sure the new advertising token from refresh looks right AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); try { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); + if (useRefreshedV4Uid) { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); + } else { + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt.currentSalt()), adTokenFromRefresh.userIdentity.id); + } } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); + testContext.failNow(e); + return; } String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); assertNotEquals(genRefreshToken, refreshTokenStringNew); RefreshToken refreshTokenAfterRefresh = decodeRefreshToken(encoder, refreshTokenStringNew, identityType); - assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id, salt.currentKeySalt(), false); + if (useRefreshedV4Uid) { + assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id, refreshSalt.currentKeySalt(), false); + } else { + assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id); + } assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); @@ -5384,12 +5105,12 @@ void keySharingKeysets_SHARER_defaultMaxSharingLifetimeSeconds(boolean provideSi } // Tests: - // SHARER has access to a keyset that has the same site_id as ID_READER's - direct access - // SHARER has access to a keyset with allowed_sites that includes us - access through sharing - // SHARER has no access to a keyset that is disabled - direct reject - // SHARER has no access to a keyset with a missing allowed_sites - reject by sharing - // SHARER has no access to a keyset with an empty allowed_sites - reject by sharing - // SHARER has no access to a keyset with an allowed_sites for other sites - reject by sharing +// SHARER has access to a keyset that has the same site_id as ID_READER's - direct access +// SHARER has access to a keyset with allowed_sites that includes us - access through sharing +// SHARER has no access to a keyset that is disabled - direct reject +// SHARER has no access to a keyset with a missing allowed_sites - reject by sharing +// SHARER has no access to a keyset with an empty allowed_sites - reject by sharing +// SHARER has no access to a keyset with an allowed_sites for other sites - reject by sharing void keySharingKeysets_SHARER(boolean provideSiteDomainNames, boolean provideAppNames, Vertx vertx, VertxTestContext testContext, int expectedMaxSharingLifetimeSeconds) { if (!provideAppNames) { this.uidOperatorVerticle.setKeySharingEndpointProvideAppNames(false); From 43cecc7c745f8d3366c10ff7265a1a275fa17e68 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 02:56:55 +0800 Subject: [PATCH 14/38] Added v2/identity/map and v3/identity/map optout tests --- .../operator/UIDOperatorVerticleTest.java | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 63dfc1200..e4961a455 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -1010,7 +1010,7 @@ void fallbackToBase64DecodingIfBinaryEnvelopeDecodeFails(Vertx vertx, VertxTestC } @ParameterizedTest - @MethodSource("policyParameters") + @ValueSource(strings = {"policy", "optout_check"}) void identityMapNewClientWrongPolicySpecified(String policyParameterKey, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, newClientCreationDateTime, Role.MAPPER); @@ -1339,21 +1339,21 @@ void v3IdentityMapMixedInputSuccess( }, Map.of(HttpHeaders.CONTENT_TYPE.toString(), contentType)); } - @Test - void v3IdentityMapUnmappedIdentitiesOptoutAndInvalid(Vertx vertx, VertxTestContext testContext) { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void v3IdentityMapUnmappedIdentitiesOptoutAndInvalid(boolean useV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); + + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } // optout when(this.optOutStore.getLatestEntry(any())).thenReturn(Instant.now()); - Instant lastUpdated = Instant.now().minus(1, DAYS); - Instant refreshFrom = lastUpdated.plus(30, DAYS); - - SaltEntry salt = new SaltEntry(1, "1", lastUpdated.toEpochMilli(), "salt", refreshFrom.toEpochMilli(), "previousSalt", null, null); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); - JsonObject request = new JsonObject(""" { "email": ["test1@uid2.com", "invalid_email"] } """ @@ -3324,13 +3324,20 @@ void tokenGenerateRespectOptOutOption(String policyParameterKey, Vertx vertx, Ve }); } - @Test - void identityMapDefaultOption(Vertx vertx, VertxTestContext testContext) { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void identityMapOptoutDefaultOption(boolean useV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } + else { + setupSalts(); + } + // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(now.minus(1, ChronoUnit.HOURS)); @@ -3355,18 +3362,25 @@ void identityMapDefaultOption(Vertx vertx, VertxTestContext testContext) { }); } - private static Stream policyParameters() { - return Stream.of("policy", "optout_check"); - } - @ParameterizedTest - @MethodSource("policyParameters") - void identityMapRespectOptOutOption(String policyParameterKey, Vertx vertx, VertxTestContext testContext) { + @CsvSource(value = { + "true,policy", + "true,optout_check", + + "false,policy", + "false,optout_check" + }) + void identityMapRespectOptOutOption(boolean useV4Uid, String policyParameterKey, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(now.minus(1, ChronoUnit.HOURS)); From ac597c840e18f39044c9f86be7f460d803ec0b3f Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 03:01:56 +0800 Subject: [PATCH 15/38] Added v2/token/generate and v2/token/refresh optout tests --- .../operator/UIDOperatorVerticleTest.java | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index e4961a455..f06004869 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -2378,10 +2378,18 @@ void tokenRefreshExpiredTokenUnauthenticated(Vertx vertx, VertxTestContext testC }); } - @Test - void tokenRefreshOptOut(Vertx vertx, VertxTestContext testContext) { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void tokenRefreshOptOut(boolean useV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; + + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + generateRefreshToken(vertx, "email", emailAddress, clientSiteId, genRespJson -> { JsonObject bodyJson = genRespJson.getJsonObject("body"); String refreshToken = bodyJson.getString("refresh_token"); @@ -3294,13 +3302,24 @@ void identityMapBatchRequestTooLargeForPhone(Vertx vertx, VertxTestContext testC } @ParameterizedTest - @ValueSource(strings = {"policy", "optout_check"}) - void tokenGenerateRespectOptOutOption(String policyParameterKey, Vertx vertx, VertxTestContext testContext) { + @CsvSource(value = { + "true,policy", + "true,optout_check", + + "false,policy", + "false,optout_check" + }) + void tokenGenerateRespectOptOutOption(boolean useV4Uid, String policyParameterKey, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.GENERATOR); - setupSalts(); setupKeys(); + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(now.minus(1, ChronoUnit.HOURS)); @@ -3333,8 +3352,7 @@ void identityMapOptoutDefaultOption(boolean useV4Uid, Vertx vertx, VertxTestCont if (useV4Uid) { setupSaltsForV4UidAndV4PrevUid(); - } - else { + } else { setupSalts(); } From f00359f5afd3d7f7142b7184ec7e3f19f3f6da25 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 03:24:10 +0800 Subject: [PATCH 16/38] Added v2/token/logout test --- .../uid2/operator/UIDOperatorVerticleTest.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index f06004869..882cf4a47 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -2805,13 +2805,24 @@ void optOutStatusUnauthorized(String contentType, Vertx vertx, VertxTestContext } @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void logoutV2(String contentType, Vertx vertx, VertxTestContext testContext) { + @CsvSource(value = { + "true,text/plain", + "true,application/octet-stream", + + "false,text/plain", + "false,application/octet-stream" + }) + void logoutV2(boolean useV4Uid, String contentType, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.OPTOUT); - setupSalts(); setupKeys(); + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + JsonObject req = new JsonObject(); req.put("email", "test@uid2.com"); From 9f1d46a5e4315811fdae1e7840fa4436dc3af65e Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 03:46:52 +0800 Subject: [PATCH 17/38] Updated formatting --- src/main/java/com/uid2/operator/model/IdentityRequest.java | 3 +-- src/main/java/com/uid2/operator/model/MapRequest.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/uid2/operator/model/IdentityRequest.java b/src/main/java/com/uid2/operator/model/IdentityRequest.java index 769ca14ca..4a91f1e55 100644 --- a/src/main/java/com/uid2/operator/model/IdentityRequest.java +++ b/src/main/java/com/uid2/operator/model/IdentityRequest.java @@ -10,8 +10,7 @@ public IdentityRequest( PublisherIdentity publisherIdentity, UserIdentity userIdentity, OptoutCheckPolicy tokenGeneratePolicy, - IdentityEnvironment identityEnvironment) - { + IdentityEnvironment identityEnvironment) { this.publisherIdentity = publisherIdentity; this.userIdentity = userIdentity; this.optoutCheckPolicy = tokenGeneratePolicy; diff --git a/src/main/java/com/uid2/operator/model/MapRequest.java b/src/main/java/com/uid2/operator/model/MapRequest.java index 7283a9334..22debc6b9 100644 --- a/src/main/java/com/uid2/operator/model/MapRequest.java +++ b/src/main/java/com/uid2/operator/model/MapRequest.java @@ -12,8 +12,7 @@ public MapRequest( UserIdentity userIdentity, OptoutCheckPolicy optoutCheckPolicy, Instant asOf, - IdentityEnvironment identityEnvironment) - { + IdentityEnvironment identityEnvironment) { this.userIdentity = userIdentity; this.optoutCheckPolicy = optoutCheckPolicy; this.asOf = asOf; From 4e3c31b2188bc8ccd7bb5a220b03a817a4365348 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 03:51:21 +0800 Subject: [PATCH 18/38] Updated identity enums --- .../operator/model/IdentityEnvironment.java | 4 ++- .../uid2/operator/model/IdentityScope.java | 30 +++++++++++-------- .../com/uid2/operator/model/IdentityType.java | 23 +++++++++----- .../uid2/operator/model/IdentityVersion.java | 13 ++++++-- .../service/EncryptedTokenEncoder.java | 2 +- .../com/uid2/operator/service/TokenUtils.java | 6 ++-- 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/uid2/operator/model/IdentityEnvironment.java b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java index f58e80836..8e2b44b45 100644 --- a/src/main/java/com/uid2/operator/model/IdentityEnvironment.java +++ b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java @@ -4,7 +4,9 @@ import com.uid2.operator.vertx.ClientInputValidationException; public enum IdentityEnvironment { - TEST(0), INTEG(1), PROD(2); + TEST(0), + INTEG(1), + PROD(2); private final int value; diff --git a/src/main/java/com/uid2/operator/model/IdentityScope.java b/src/main/java/com/uid2/operator/model/IdentityScope.java index 0bff1edc1..a9d6cae5e 100644 --- a/src/main/java/com/uid2/operator/model/IdentityScope.java +++ b/src/main/java/com/uid2/operator/model/IdentityScope.java @@ -6,23 +6,29 @@ public enum IdentityScope { UID2(0), EUID(1); - public final int value; + private final int value; - IdentityScope(int value) { this.value = value; } + IdentityScope(int value) { + this.value = value; + } + + public int getValue() { + return value; + } public static IdentityScope fromValue(int value) { - switch (value) { - case 0: return UID2; - case 1: return EUID; - default: throw new ClientInputValidationException("Invalid value for IdentityScope: " + value); - } + return switch (value) { + case 0 -> UID2; + case 1 -> EUID; + default -> throw new ClientInputValidationException("Invalid value for IdentityScope: " + value); + }; } public static IdentityScope fromString(String str) { - switch (str.toLowerCase()) { - case "uid2": return UID2; - case "euid": return EUID; - default: throw new ClientInputValidationException("Invalid string for IdentityScope: " + str); - } + return switch (str.toLowerCase()) { + case "uid2" -> UID2; + case "euid" -> EUID; + default -> throw new ClientInputValidationException("Invalid string for IdentityScope: " + str); + }; } } diff --git a/src/main/java/com/uid2/operator/model/IdentityType.java b/src/main/java/com/uid2/operator/model/IdentityType.java index b64817df5..aaf3bcc54 100644 --- a/src/main/java/com/uid2/operator/model/IdentityType.java +++ b/src/main/java/com/uid2/operator/model/IdentityType.java @@ -3,17 +3,24 @@ import com.uid2.operator.vertx.ClientInputValidationException; public enum IdentityType { - Email(0), Phone(1); + Email(0), + Phone(1); - public final int value; + private final int value; - IdentityType(int value) { this.value = value; } + IdentityType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } public static IdentityType fromValue(int value) { - switch (value) { - case 0: return Email; - case 1: return Phone; - default: throw new ClientInputValidationException("Invalid valid for IdentityType: " + value); - } + return switch (value) { + case 0 -> Email; + case 1 -> Phone; + default -> throw new ClientInputValidationException("Invalid valid for IdentityType: " + value); + }; } } diff --git a/src/main/java/com/uid2/operator/model/IdentityVersion.java b/src/main/java/com/uid2/operator/model/IdentityVersion.java index 995071027..2c34ac8e6 100644 --- a/src/main/java/com/uid2/operator/model/IdentityVersion.java +++ b/src/main/java/com/uid2/operator/model/IdentityVersion.java @@ -3,11 +3,18 @@ import com.uid2.operator.vertx.ClientInputValidationException; public enum IdentityVersion { - V3(0), V4(1); + V3(0), + V4(1); - public final int value; + private final int value; - IdentityVersion(int value) { this.value = value; } + IdentityVersion(int value) { + this.value = value; + } + + public int getValue() { + return value; + } public static IdentityVersion fromValue(int value) { return switch (value) { diff --git a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java index 3e2b6e875..402b672b3 100644 --- a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java +++ b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java @@ -365,7 +365,7 @@ private byte[] encryptIdentityV2(PublisherIdentity publisherIdentity, UserIdenti } private static byte encodeIdentityTypeV3(UserIdentity userIdentity) { - return (byte) (TokenUtils.encodeIdentityScope(userIdentity.identityScope) | (userIdentity.identityType.value << 2) | 3); + return (byte) (TokenUtils.encodeIdentityScope(userIdentity.identityScope) | (userIdentity.identityType.getValue() << 2) | 3); // "| 3" is used so that the 2nd char matches the version when V3 or higher. Eg "3" for V3 and "4" for V4 } diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index 848ef3526..9516fe312 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -72,15 +72,15 @@ public static byte[] getAdvertisingIdV4FromIdentityHash(IdentityScope scope, Ide } public static byte encodeIdentityScope(IdentityScope identityScope) { - return (byte) (identityScope.value << 4); + return (byte) (identityScope.getValue() << 4); } public static byte encodeIdentityType(IdentityType identityType) { - return (byte) (identityType.value << 2); + return (byte) (identityType.getValue() << 2); } public static byte encodeIdentityVersion(IdentityVersion identityVersion) { - return (byte) (identityVersion.value << 6); + return (byte) (identityVersion.getValue() << 6); } public static byte encodeIdentityEnvironment(IdentityEnvironment identityEnvironment) { From 661335063a7714258212e5b79a7f228f5b8f12e6 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 04:02:57 +0800 Subject: [PATCH 19/38] Updated formatting --- .../com/uid2/operator/model/UserIdentity.java | 3 +-- .../com/uid2/operator/service/InputUtil.java | 16 +++++++------- .../operator/service/UIDOperatorService.java | 15 +++++-------- .../operator/vertx/UIDOperatorVerticle.java | 11 ++++------ .../uid2/operator/UIDOperatorServiceTest.java | 12 ++++------- .../operator/UIDOperatorVerticleTest.java | 21 +++++++++---------- 6 files changed, 31 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/uid2/operator/model/UserIdentity.java b/src/main/java/com/uid2/operator/model/UserIdentity.java index fde02362a..17c0f74b8 100644 --- a/src/main/java/com/uid2/operator/model/UserIdentity.java +++ b/src/main/java/com/uid2/operator/model/UserIdentity.java @@ -11,8 +11,7 @@ public class UserIdentity { public final Instant establishedAt; public final Instant refreshedAt; - public UserIdentity(IdentityScope identityScope, IdentityType identityType, - byte[] id, int privacyBits, + public UserIdentity(IdentityScope identityScope, IdentityType identityType, byte[] id, int privacyBits, Instant establishedAt, Instant refreshedAt) { this.identityScope = identityScope; this.identityType = identityType; diff --git a/src/main/java/com/uid2/operator/service/InputUtil.java b/src/main/java/com/uid2/operator/service/InputUtil.java index 9a43840d3..33a862c38 100644 --- a/src/main/java/com/uid2/operator/service/InputUtil.java +++ b/src/main/java/com/uid2/operator/service/InputUtil.java @@ -50,8 +50,7 @@ public static InputVal normalizePhoneHash(String input) { return InputVal.invalidPhoneHash(input); } - public static boolean isAsciiDigit(char d) - { + public static boolean isAsciiDigit(char d) { return d >= '0' && d <= '9'; } @@ -80,7 +79,7 @@ public static boolean isPhoneNumberNormalized(String phoneNumber) { public static InputVal normalizeEmail(String email) { final String normalize = normalizeEmailString(email); - if (normalize != null && !normalize.isEmpty()) { + if (normalize != null && normalize.length() > 0) { return InputVal.validEmail(email, normalize); } return InputVal.invalidEmail(email); @@ -143,15 +142,15 @@ public static String normalizeEmailString(String email) { wsBuffer.append(c); break; } - if (!wsBuffer.isEmpty()) { - sb.append(wsBuffer); + if (wsBuffer.length() > 0) { + sb.append(wsBuffer.toString()); wsBuffer = new StringBuilder(); } sb.append(c); } } } - if (sb.isEmpty()) { + if (sb.length() == 0) { return null; } final String domainPart = sb.toString(); @@ -162,7 +161,7 @@ public static String normalizeEmailString(String email) { } else { addressPartToUse = preSb; } - if (addressPartToUse.isEmpty()) { + if (addressPartToUse.length() == 0) { return null; } @@ -179,7 +178,7 @@ private enum EmailParsingState { Pre, SubDomain, Domain, - Terminal, + Terminal } public static class InputVal { @@ -273,5 +272,4 @@ public UserIdentity toUserIdentity(IdentityScope identityScope, int privacyBits, establishedAt); } } - } diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index c7ed642d4..00a6a3574 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -54,8 +54,7 @@ public class UIDOperatorService implements IUIDOperatorService { private final UidInstanceIdProvider uidInstanceIdProvider; public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, - IdentityScope identityScope, - Handler saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) { + IdentityScope identityScope, Handler saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) { this.saltProvider = saltProvider; this.encoder = encoder; this.optOutStore = optOutStore; @@ -101,10 +100,8 @@ public IdentityTokens generateIdentity(IdentityRequest request, Duration refresh final Instant now = EncodingUtils.NowUTCMillis(this.clock); final byte[] firstLevelHash = getFirstLevelHash(request.userIdentity.id, now); final UserIdentity firstLevelHashIdentity = new UserIdentity( - request.userIdentity.identityScope, request.userIdentity.identityType, - firstLevelHash, request.userIdentity.privacyBits, - request.userIdentity.establishedAt, request.userIdentity.refreshedAt - ); + request.userIdentity.identityScope, request.userIdentity.identityType, firstLevelHash, request.userIdentity.privacyBits, + request.userIdentity.establishedAt, request.userIdentity.refreshedAt); if (request.shouldCheckOptOut() && getGlobalOptOutResult(firstLevelHashIdentity, false).isOptedOut()) { return IdentityTokens.LogoutToken; @@ -231,8 +228,7 @@ private MappedIdentity getMappedIdentity(UserIdentity firstLevelHashIdentity, In advertisingId, rotatingSalt.hashedId(), previousAdvertisingId, - refreshFrom - ); + refreshFrom); } private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt, SaltEntry.KeyMaterial key, IdentityEnvironment env) { @@ -296,8 +292,7 @@ private RefreshToken createRefreshToken(PublisherIdentity publisherIdentity, Use now.plusMillis(refreshExpiresAfter.toMillis()), this.operatorIdentity, publisherIdentity, - userIdentity - ); + userIdentity); } private AdvertisingToken createAdvertisingToken(PublisherIdentity publisherIdentity, UserIdentity userIdentity, Instant now, Duration identityExpiresAfter) { diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 4641ab61c..46570e4c6 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -162,10 +162,6 @@ public UIDOperatorVerticle(IConfigStore configStore, SecureLinkValidatorService secureLinkValidatorService, Handler saltRetrievalResponseHandler, UidInstanceIdProvider uidInstanceIdProvider) { - this.identityScope = IdentityScope.fromString(config.getString("identity_scope", "uid2")); - - - this.keyManager = keyManager; this.secureLinkValidatorService = secureLinkValidatorService; try { @@ -177,12 +173,13 @@ public UIDOperatorVerticle(IConfigStore configStore, this.clientSideTokenGenerate = clientSideTokenGenerate; this.healthComponent.setHealthStatus(false, "not started"); this.auth = new AuthMiddleware(clientKeyProvider); + this.encoder = new EncryptedTokenEncoder(keyManager); this.siteProvider = siteProvider; this.clientSideKeypairProvider = clientSideKeypairProvider; this.saltProvider = saltProvider; this.optOutStore = optOutStore; this.clock = clock; - this.encoder = new EncryptedTokenEncoder(keyManager); + this.identityScope = IdentityScope.fromString(config.getString("identity_scope", "uid2")); this.encryptedPayloadHandler = new V2PayloadHandler(keyManager, config.getBoolean("enable_v2_encryption", true), this.identityScope, siteProvider); this.phoneSupport = config.getBoolean("enable_phone_support", true); this.tcfVendorId = config.getInteger("tcf_vendor_id", 21); @@ -496,10 +493,10 @@ private void handleClientSideTokenGenerateImpl(RoutingContext rc) throws NoSuchA if (identityTokens.isEmptyToken()) { response = ResponseUtil.SuccessNoBodyV2(ResponseStatus.OptOut); responseStatus = TokenResponseStatsCollector.ResponseStatus.OptOut; - } else { //user not opted out and already generated valid identity token + } else { // user not opted out and already generated valid identity token response = ResponseUtil.SuccessV2(toTokenResponseJson(identityTokens)); } - //if returning an optout token or a successful identity token created originally + // if returning an optout token or a successful identity token created originally if (responseStatus == TokenResponseStatsCollector.ResponseStatus.Success) { V2RequestUtil.handleRefreshTokenInResponseBody(response.getJsonObject("body"), keyManager, this.identityScope); } diff --git a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java index 7d11e8121..1c35ac1e3 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java @@ -323,12 +323,10 @@ void testTestOptOutKeyIdentityScopeMismatch() { } @ParameterizedTest - @CsvSource({ - "Email,test@example.com,UID2", + @CsvSource({"Email,test@example.com,UID2", "Email,test@example.com,EUID", "Phone,+01010101010,UID2", - "Phone,+01010101010,EUID" - }) + "Phone,+01010101010,EUID"}) void testGenerateTokenForOptOutUser(IdentityType type, String identity, IdentityScope scope) { final UserIdentity userIdentity = createUserIdentity(identity, scope, type); @@ -802,14 +800,12 @@ void testNormalIdentityOptIn(TestIdentityInputType type, String id, IdentityScop } @ParameterizedTest - @CsvSource({ - "Email,blah@unifiedid.com,UID2", + @CsvSource({"Email,blah@unifiedid.com,UID2", "EmailHash,blah@unifiedid.com,UID2", "Phone,+61401234567,EUID", "PhoneHash,+61401234567,EUID", "Email,blah@unifiedid.com,EUID", - "EmailHash,blah@unifiedid.com,EUID" - }) + "EmailHash,blah@unifiedid.com,EUID"}) void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String id, IdentityScope scope) throws Exception { RotatingSaltProvider saltProvider = new RotatingSaltProvider( new EmbeddedResourceStorage(Main.class), diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 2649793cc..06c66a81d 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -1124,7 +1124,7 @@ void identityMapNewClientWrongPolicySpecifiedOlderKeySuccessful(String policyPar } @ParameterizedTest - @CsvSource(value = { + @CsvSource({ // After - V4 UID, V4 previous UID "true,true,text/plain", "true,true,application/octet-stream", @@ -1761,7 +1761,7 @@ void tokenGenerateOptOutTokenWithDisableOptoutTokenFF(String policyParameterKey, } @ParameterizedTest - @CsvSource(value = { + @CsvSource({ "true,text/plain", "true,application/octet-stream", @@ -1856,7 +1856,7 @@ void tokenGenerateForEmailHash(Vertx vertx, VertxTestContext testContext) { } @ParameterizedTest - @CsvSource(value = { + @CsvSource({ // Before - v4 UID, v4 refreshed UID "true,true,text/plain", "true,true,application/octet-stream", @@ -2806,7 +2806,7 @@ void optOutStatusUnauthorized(String contentType, Vertx vertx, VertxTestContext } @ParameterizedTest - @CsvSource(value = { + @CsvSource({ "true,text/plain", "true,application/octet-stream", @@ -3314,7 +3314,7 @@ void identityMapBatchRequestTooLargeForPhone(Vertx vertx, VertxTestContext testC } @ParameterizedTest - @CsvSource(value = { + @CsvSource({ "true,policy", "true,optout_check", @@ -3393,7 +3393,7 @@ void identityMapOptoutDefaultOption(boolean useV4Uid, Vertx vertx, VertxTestCont } @ParameterizedTest - @CsvSource(value = { + @CsvSource({ "true,policy", "true,optout_check", @@ -4286,7 +4286,7 @@ private Tuple.Tuple2 createClientSideTokenGenerateRequest identity.put("email_hash", getSha256(rawId)); } else if (identityType == IdentityType.Phone) { identity.put("phone_hash", getSha256(rawId)); - } else { //can't be other types + } else { // can't be other types org.junit.jupiter.api.Assertions.fail("Identity type is not: [email_hash,phone_hash]"); } @@ -4298,7 +4298,6 @@ private Tuple.Tuple2 createClientSideTokenGenerateRequest return createClientSideTokenGenerateRequestWithPayload(identity, timestamp, null); } - @ParameterizedTest @CsvSource({ "test@example.com,Email", @@ -4347,9 +4346,9 @@ void cstgUserOptsOutAfterTokenGenerate(String id, IdentityType identityType, Ver } // tests for opted out user should lead to generating ad tokens with optout success response -// tests for non-opted out user should generate the UID2 identity and the generated refresh token can be -// refreshed again -// tests for all email/phone combos + // tests for non-opted out user should generate the UID2 identity and the generated refresh token can be + // refreshed again + // tests for all email/phone combos @ParameterizedTest @CsvSource({ // After - v4 UID, refreshed v4 UID From c702d414b056d48182b446395b61454aafcdd43f Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 10:55:06 +0800 Subject: [PATCH 20/38] Updated mock salt files --- src/main/resources/com.uid2.core/test/salts/metadata.json | 5 ++--- .../resources/com.uid2.core/test/salts/metadataExpired.json | 4 ++-- .../com.uid2.core/test/salts/salts.txt.1670796729291 | 5 ++++- .../com.uid2.core/test/salts/salts.txt.1745907348982 | 5 ++++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/resources/com.uid2.core/test/salts/metadata.json b/src/main/resources/com.uid2.core/test/salts/metadata.json index 267c63cb2..06313d890 100644 --- a/src/main/resources/com.uid2.core/test/salts/metadata.json +++ b/src/main/resources/com.uid2.core/test/salts/metadata.json @@ -9,13 +9,12 @@ "effective" : 1670796729291, "expires" : 1766125493000, "location" : "/com.uid2.core/test/salts/salts.txt.1670796729291", - "size" : 2 + "size" : 5 },{ "effective" : 1745907348982, "expires" : 1766720293000, "location" : "/com.uid2.core/test/salts/salts.txt.1745907348982", - "size" : 2 + "size" : 5 } ] } - diff --git a/src/main/resources/com.uid2.core/test/salts/metadataExpired.json b/src/main/resources/com.uid2.core/test/salts/metadataExpired.json index 282989606..577d3c175 100644 --- a/src/main/resources/com.uid2.core/test/salts/metadataExpired.json +++ b/src/main/resources/com.uid2.core/test/salts/metadataExpired.json @@ -8,6 +8,6 @@ "effective" : 1670796729291, "expires" : 1670796729292, "location" : "/com.uid2.core/test/salts/salts.txt.1670796729291", - "size" : 2 + "size" : 5 }] -} \ No newline at end of file +} diff --git a/src/main/resources/com.uid2.core/test/salts/salts.txt.1670796729291 b/src/main/resources/com.uid2.core/test/salts/salts.txt.1670796729291 index 992afbb45..15a91aa85 100644 --- a/src/main/resources/com.uid2.core/test/salts/salts.txt.1670796729291 +++ b/src/main/resources/com.uid2.core/test/salts/salts.txt.1670796729291 @@ -1,2 +1,5 @@ 1000000,1806364800001,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=,1814140800000,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnsS=,,,,,, -1000001,1786924800001,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnst=,1812844800000,,,,,,, \ No newline at end of file +1000001,1786924800001,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnst=,1812844800000,,,,,,, +1000002,1798588800001,,1806364800000,,2100002,key12345key12345key12345key12340,salt1234salt1234salt1234salt1230,2000002,key12345key12345key12345key12345,salt1234salt1234salt1234salt1234 +1000003,1795996800001,,1803772800000,,2000003,key12345key12345key12345key12346,salt1234salt1234salt1234salt1235,,, +1000004,1811548800001,,1819324800000,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnsw=,2000004,key12345key12345key12345key12347,salt1234salt1234salt1234salt1236,,, diff --git a/src/main/resources/com.uid2.core/test/salts/salts.txt.1745907348982 b/src/main/resources/com.uid2.core/test/salts/salts.txt.1745907348982 index 992afbb45..15a91aa85 100644 --- a/src/main/resources/com.uid2.core/test/salts/salts.txt.1745907348982 +++ b/src/main/resources/com.uid2.core/test/salts/salts.txt.1745907348982 @@ -1,2 +1,5 @@ 1000000,1806364800001,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=,1814140800000,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnsS=,,,,,, -1000001,1786924800001,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnst=,1812844800000,,,,,,, \ No newline at end of file +1000001,1786924800001,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnst=,1812844800000,,,,,,, +1000002,1798588800001,,1806364800000,,2100002,key12345key12345key12345key12340,salt1234salt1234salt1234salt1230,2000002,key12345key12345key12345key12345,salt1234salt1234salt1234salt1234 +1000003,1795996800001,,1803772800000,,2000003,key12345key12345key12345key12346,salt1234salt1234salt1234salt1235,,, +1000004,1811548800001,,1819324800000,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnsw=,2000004,key12345key12345key12345key12347,salt1234salt1234salt1234salt1236,,, From ff8771890d9f7907aca265c5ca6e6e9e1a5f99b7 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 15:36:05 +0800 Subject: [PATCH 21/38] Added v2/token/generate and v2/token/client-generate refresh optout tests --- .../com/uid2/operator/model/KeyManager.java | 3 +- .../operator/UIDOperatorVerticleTest.java | 87 ++++++++++++------- 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/uid2/operator/model/KeyManager.java b/src/main/java/com/uid2/operator/model/KeyManager.java index 19bae8d07..8fb879222 100644 --- a/src/main/java/com/uid2/operator/model/KeyManager.java +++ b/src/main/java/com/uid2/operator/model/KeyManager.java @@ -16,7 +16,7 @@ import java.util.stream.Collectors; public class KeyManager { - private static final Logger LOGGER = LoggerFactory.getLogger(UIDOperatorVerticle.class); + private static final Logger LOGGER = LoggerFactory.getLogger(KeyManager.class); private final IKeysetKeyStore keysetKeyStore; private final RotatingKeysetProvider keysetProvider; @@ -76,7 +76,6 @@ public KeysetKey getKey(int keyId) { return this.keysetKeyStore.getSnapshot().getKey(keyId); } - public List getKeysForSharingOrDsps() { Map keysetMap = this.keysetProvider.getSnapshot().getAllKeysets(); List keys = keysetKeyStore.getSnapshot().getAllKeysetKeys(); diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 06c66a81d..11049f85e 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -137,10 +137,11 @@ public class UIDOperatorVerticleTest { private IConfigStore configStore; private UidInstanceIdProvider uidInstanceIdProvider; + private final JsonObject config = new JsonObject(); private SimpleMeterRegistry registry; private ExtendedUIDOperatorVerticle uidOperatorVerticle; private RuntimeConfig runtimeConfig; - private final JsonObject config = new JsonObject(); + private EncryptedTokenEncoder encoder; @BeforeEach void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo testInfo) { @@ -169,6 +170,8 @@ void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo testInfo this.registry = new SimpleMeterRegistry(); Metrics.globalRegistry.add(registry); + + this.encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); } @AfterEach @@ -1673,8 +1676,6 @@ void tokenGenerateOptOutToken(String policyParameterKey, String identity, Identi decodeV2RefreshToken(json); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, identityType); RefreshToken refreshToken = encoder.decodeRefreshToken(body.getString("decrypted_refresh_token")); final byte[] advertisingId = getAdvertisingIdFromIdentity(identityType, @@ -1775,7 +1776,6 @@ void tokenGenerateForEmail(boolean useV4Uid, String contentType, Vertx vertx, Ve setupKeys(); SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -1834,7 +1834,6 @@ void tokenGenerateForEmailHash(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -1882,7 +1881,6 @@ void tokenGenerateThenRefresh( setupKeys(); SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, HttpHeaders.CONTENT_TYPE.toString(), contentType); @@ -1982,7 +1980,6 @@ void tokenGenerateThenRefreshSaltsExpired(Vertx vertx, VertxTestContext testCont assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Email); @@ -2057,7 +2054,6 @@ void tokenGenerateThenValidateWithEmail_Match(boolean useV4Uid, Vertx vertx, Ver setupKeys(); SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); generateTokens(vertx, "email", emailAddress, genRespJson -> { assertEquals("success", genRespJson.getString("status")); @@ -2165,7 +2161,6 @@ void tokenGenerateUsingCustomSiteKey(Vertx vertx, VertxTestContext testContext) assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); @@ -2196,7 +2191,6 @@ void tokenGenerateSaltsExpired(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); @@ -2281,13 +2275,23 @@ void tokenRefreshInvalidTokenUnauthenticated(Vertx vertx, VertxTestContext testC }); } - private void generateRefreshToken(Vertx vertx, String identityType, String identity, int siteId, Handler handler) { + private void generateRefreshToken(Vertx vertx, String identityType, String identity, int siteId, boolean useV4Uid, Handler handler) { fakeAuth(siteId, Role.GENERATOR); - setupSalts(); setupKeys(); + + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + generateTokens(vertx, identityType, identity, handler); } + private void generateRefreshToken(Vertx vertx, String identityType, String identity, int siteId, Handler handler) { + generateRefreshToken(vertx, identityType, identity, siteId, false, handler); + } + @Test void captureDurationsBetweenRefresh(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; @@ -2409,17 +2413,29 @@ void tokenRefreshOptOut(boolean useV4Uid, Vertx vertx, VertxTestContext testCont }); } - @Test - void tokenRefreshOptOutBeforeLogin(Vertx vertx, VertxTestContext testContext) { + @ParameterizedTest + @CsvSource({ + "true,true", + "true,false", + "false,true", + "false,false" + }) + void tokenRefreshOptOutBeforeLogin(boolean useV4Uid, boolean useRefreshedV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; - generateRefreshToken(vertx, "email", emailAddress, clientSiteId, genRespJson -> { + generateRefreshToken(vertx, "email", emailAddress, clientSiteId, useV4Uid, genRespJson -> { JsonObject bodyJson = genRespJson.getJsonObject("body"); String refreshToken = bodyJson.getString("refresh_token"); String refreshTokenDecryptSecret = bodyJson.getString("refresh_response_key"); when(this.optOutStore.getLatestEntry(any())).thenReturn(now.minusSeconds(10)); + if (useRefreshedV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + sendTokenRefresh(vertx, testContext, refreshToken, refreshTokenDecryptSecret, 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); assertNull(refreshRespJson.getJsonObject("body")); @@ -2948,7 +2964,6 @@ void tokenGenerateForPhone(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Phone); @@ -2985,7 +3000,6 @@ void tokenGenerateForPhoneHash(Vertx vertx, VertxTestContext testContext) { assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Phone); @@ -3027,7 +3041,6 @@ void tokenGenerateThenRefreshForPhone(Vertx vertx, VertxTestContext testContext) assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Phone); @@ -3764,7 +3777,6 @@ void cstgDomainNameCheckPasses(String httpOrigin, Vertx vertx, VertxTestContext JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct testContext.completeNow(); }); @@ -3791,7 +3803,6 @@ void cstgAppNameCheckPasses(String appName, Vertx vertx, VertxTestContext testCo JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct assertTokenStatusMetrics( clientSideTokenGenerateSiteId, @@ -4300,19 +4311,31 @@ private Tuple.Tuple2 createClientSideTokenGenerateRequest @ParameterizedTest @CsvSource({ - "test@example.com,Email", - "+61400000000,Phone" + "true,true,test@example.com,Email", + "true,true,+61400000000,Phone", + + "true,false,test@example.com,Email", + "true,false,+61400000000,Phone", + + "false,true,test@example.com,Email", + "false,true,+61400000000,Phone", + + "false,false,test@example.com,Email", + "false,false,+61400000000,Phone" }) - void cstgUserOptsOutAfterTokenGenerate(String id, IdentityType identityType, Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { + void cstgUserOptsOutAfterTokenGenerate( + boolean useV4Uid, boolean useRefreshedV4Uid, String id, IdentityType identityType, + Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); + SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + final Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); // When we generate the token the user hasn't opted out. when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(null); - final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(UserIdentity.class); sendCstg(vertx, @@ -4332,12 +4355,22 @@ void cstgUserOptsOutAfterTokenGenerate(String id, IdentityType identityType, Ver final AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); final RefreshToken refreshToken = decodeRefreshToken(encoder, decodeV2RefreshToken(response), identityType); - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); + if (useV4Uid) { + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt.currentKeySalt(), false); + } else { + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); + } // When we refresh the token the user has opted out. when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(advertisingToken.userIdentity.establishedAt.plusSeconds(1)); + if (useRefreshedV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + sendTokenRefresh(vertx, testContext, genBody.getString("refresh_token"), genBody.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); testContext.completeNow(); @@ -4382,7 +4415,6 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( setupCstgBackend("cstg.co.uk"); SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); @@ -4513,7 +4545,6 @@ void cstgSaltsExpired(String httpOrigin, Vertx vertx, VertxTestContext testConte JsonObject refreshBody = respJson.getJsonObject("body"); assertNotNull(refreshBody); - var encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); validateAndGetToken(encoder, refreshBody, IdentityType.Email); //to validate token version is correct verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(true); @@ -4842,7 +4873,6 @@ void tokenGenerateRotatingKeysets_GENERATOR(String testRun, Vertx vertx, VertxTe assertEquals("success", json.getString("status")); JsonObject body = json.getJsonObject("body"); assertNotNull(body); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); @@ -5027,7 +5057,6 @@ public void keyBidstreamReturnsCustomMaxBidstreamLifetimeHeader(Vertx vertx, Ver } } - private static Stream testKeyDownloadEndpointKeysetsData_IDREADER() { int[] expectedSiteIds = new int[]{101, 102}; int[] allMockedSiteIds = new int[]{101, 102, 103, 105}; From 01389b1dfeb314a4b15376d85b84cdac45795702 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 4 Sep 2025 17:58:15 +0800 Subject: [PATCH 22/38] Added v2/optout/status tests --- .../operator/UIDOperatorVerticleTest.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 11049f85e..0f93ef4a3 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -2737,20 +2737,30 @@ private static Stream optOutStatusRequestData() { optedOutIdsCase2.put(rawUIDS.get(2), -1L); optedOutIdsCase2.put(rawUIDS.get(3), -1L); return Stream.of( - Arguments.arguments(optedOutIdsCase1, 2, Role.MAPPER), - Arguments.arguments(optedOutIdsCase1, 2, Role.ID_READER), - Arguments.arguments(optedOutIdsCase1, 2, Role.SHARER), - Arguments.arguments(optedOutIdsCase2, 0, Role.MAPPER) + Arguments.arguments(true, optedOutIdsCase1, 2, Role.MAPPER), + Arguments.arguments(true, optedOutIdsCase1, 2, Role.ID_READER), + Arguments.arguments(true, optedOutIdsCase1, 2, Role.SHARER), + Arguments.arguments(true, optedOutIdsCase2, 0, Role.MAPPER), + + Arguments.arguments(false, optedOutIdsCase1, 2, Role.MAPPER), + Arguments.arguments(false, optedOutIdsCase1, 2, Role.ID_READER), + Arguments.arguments(false, optedOutIdsCase1, 2, Role.SHARER), + Arguments.arguments(false, optedOutIdsCase2, 0, Role.MAPPER) ); } @ParameterizedTest @MethodSource("optOutStatusRequestData") - void optOutStatusRequest(Map optedOutIds, int optedOutCount, Role role, Vertx vertx, VertxTestContext testContext) { + void optOutStatusRequest(boolean useV4Uid, Map optedOutIds, int optedOutCount, Role role, Vertx vertx, VertxTestContext testContext) { fakeAuth(126, role); - setupSalts(); setupKeys(); + if (useV4Uid) { + setupSaltsForV4UidAndV4PrevUid(); + } else { + setupSalts(); + } + JsonArray rawUIDs = new JsonArray(); for (String rawUID2 : optedOutIds.keySet()) { when(this.optOutStore.getOptOutTimestampByAdId(rawUID2)).thenReturn(optedOutIds.get(rawUID2)); From a04bc249cf20d06739928bec5bddf9275ae216fa Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 16 Sep 2025 13:57:52 +0800 Subject: [PATCH 23/38] Fixed v4 metadata and added tests --- .../com/uid2/operator/service/TokenUtils.java | 10 +++-- .../uid2/operator/service/TokenUtilsTest.java | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/uid2/operator/service/TokenUtilsTest.java diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index 9516fe312..2ce744078 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -59,7 +59,7 @@ public static byte[] getAdvertisingIdV3FromIdentityHash(IdentityScope scope, Ide } public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte[] firstLevelHash, SaltEntry.KeyMaterial encryptingKey) throws Exception { - byte metadata = (byte) (encodeIdentityVersion(IdentityVersion.V4) | encodeIdentityScope(scope) | encodeIdentityType(type) | encodeIdentityEnvironment(environment)); + byte metadata = encodeV4Metadata(scope, type, environment); return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), encryptingKey.salt()); } @@ -71,6 +71,10 @@ public static byte[] getAdvertisingIdV4FromIdentityHash(IdentityScope scope, Ide return getAdvertisingIdV4(scope, type, environment, getFirstLevelHashFromIdentityHash(identityString, firstLevelSalt), encryptingKey); } + public static byte encodeV4Metadata(IdentityScope scope, IdentityType type, IdentityEnvironment environment) { + return (byte) (encodeIdentityVersion(IdentityVersion.V4) | encodeIdentityScope(scope) | encodeIdentityType(type) | encodeIdentityEnvironment(environment)); + } + public static byte encodeIdentityScope(IdentityScope identityScope) { return (byte) (identityScope.getValue() << 4); } @@ -80,10 +84,10 @@ public static byte encodeIdentityType(IdentityType identityType) { } public static byte encodeIdentityVersion(IdentityVersion identityVersion) { - return (byte) (identityVersion.getValue() << 6); + return (byte) (identityVersion.getValue() << 5); } public static byte encodeIdentityEnvironment(IdentityEnvironment identityEnvironment) { - return (byte) (identityEnvironment.getValue()); + return (byte) (identityEnvironment.getValue() << 6); } } diff --git a/src/test/java/com/uid2/operator/service/TokenUtilsTest.java b/src/test/java/com/uid2/operator/service/TokenUtilsTest.java new file mode 100644 index 000000000..fdf663d51 --- /dev/null +++ b/src/test/java/com/uid2/operator/service/TokenUtilsTest.java @@ -0,0 +1,41 @@ +package com.uid2.operator.service; + +import com.uid2.operator.model.IdentityEnvironment; +import com.uid2.operator.model.IdentityScope; +import com.uid2.operator.model.IdentityType; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TokenUtilsTest { + @ParameterizedTest + @MethodSource("v4Metadata") + void testEncodeV4Metadata(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte expectedMetadata) { + byte metadata = TokenUtils.encodeV4Metadata(scope, type, environment); + + assertEquals(expectedMetadata, metadata); + } + + private static Stream v4Metadata() { + return Stream.of( + Arguments.of(IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.TEST, (byte) 0b00100000), + Arguments.of(IdentityScope.UID2, IdentityType.Phone, IdentityEnvironment.TEST, (byte) 0b00100100), + Arguments.of(IdentityScope.EUID, IdentityType.Email, IdentityEnvironment.TEST, (byte) 0b00110000), + Arguments.of(IdentityScope.EUID, IdentityType.Phone, IdentityEnvironment.TEST, (byte) 0b00110100), + + Arguments.of(IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.INTEG, (byte) 0b01100000), + Arguments.of(IdentityScope.UID2, IdentityType.Phone, IdentityEnvironment.INTEG, (byte) 0b01100100), + Arguments.of(IdentityScope.EUID, IdentityType.Email, IdentityEnvironment.INTEG, (byte) 0b01110000), + Arguments.of(IdentityScope.EUID, IdentityType.Phone, IdentityEnvironment.INTEG, (byte) 0b01110100), + + Arguments.of(IdentityScope.UID2, IdentityType.Email, IdentityEnvironment.PROD, (byte) 0b10100000), + Arguments.of(IdentityScope.UID2, IdentityType.Phone, IdentityEnvironment.PROD, (byte) 0b10100100), + Arguments.of(IdentityScope.EUID, IdentityType.Email, IdentityEnvironment.PROD, (byte) 0b10110000), + Arguments.of(IdentityScope.EUID, IdentityType.Phone, IdentityEnvironment.PROD, (byte) 0b10110100) + ); + } +} From fea2a8f4137ad304c7f93269682a3757f9cd38fb Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 16 Sep 2025 15:07:05 +0800 Subject: [PATCH 24/38] Updated null salt check and cleaned up test code --- .../operator/service/UIDOperatorService.java | 5 +- .../operator/UIDOperatorVerticleTest.java | 514 +++++------------- 2 files changed, 152 insertions(+), 367 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index 00a6a3574..be44950bc 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -249,10 +249,9 @@ private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt private byte[] getPreviousAdvertisingId(UserIdentity firstLevelHashIdentity, SaltEntry rotatingSalt, Instant asOf, IdentityEnvironment env) { long age = asOf.toEpochMilli() - rotatingSalt.lastUpdated(); if (age / DAY_IN_MS < 90) { - boolean missingSalt = rotatingSalt.previousSalt() == null || rotatingSalt.previousSalt().isBlank(); + boolean missingSalt = rotatingSalt.previousSalt() == null; boolean missingKey = rotatingSalt.previousKeySalt() == null - || rotatingSalt.previousKeySalt().key() == null || rotatingSalt.previousKeySalt().key().isBlank() - || rotatingSalt.previousKeySalt().salt() == null || rotatingSalt.previousKeySalt().salt().isBlank(); + || rotatingSalt.previousKeySalt().key() == null || rotatingSalt.previousKeySalt().salt() == null; if (missingSalt && missingKey) { return null; diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 0f93ef4a3..51a4b763b 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -495,24 +495,7 @@ public String getPath() { } } - private void checkIdentityMapResponse(JsonObject response, String... expectedIdentifiers) { - assertEquals("success", response.getString("status")); - - JsonObject body = response.getJsonObject("body"); - JsonArray mapped = body.getJsonArray("mapped"); - assertNotNull(mapped); - assertEquals(expectedIdentifiers.length, mapped.size()); - - for (int i = 0; i < expectedIdentifiers.length; ++i) { - String expectedIdentifier = expectedIdentifiers[i]; - JsonObject actualMap = mapped.getJsonObject(i); - assertEquals(expectedIdentifier, actualMap.getString("identifier")); - assertFalse(actualMap.getString("advertising_id").isEmpty()); - assertFalse(actualMap.getString("bucket_id").isEmpty()); - } - } - - private void checkIdentityMapResponseWithV4Uid(JsonObject response, SaltEntry salt, String... expectedIdentifiers) { + private void checkIdentityMapResponse(JsonObject response, SaltEntry salt, boolean useV4Uid, IdentityType identityType, boolean useHash, String... expectedIdentifiers) { assertEquals("success", response.getString("status")); JsonObject body = response.getJsonObject("body"); @@ -525,7 +508,11 @@ private void checkIdentityMapResponseWithV4Uid(JsonObject response, SaltEntry sa JsonObject actualMap = mapped.getJsonObject(i); assertEquals(expectedIdentifier, actualMap.getString("identifier")); try { - assertEquals(actualMap.getString("advertising_id"), EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, expectedIdentifier, firstLevelSalt, salt.currentKeySalt()))); + if (useHash) { + assertEquals(EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(identityType, expectedIdentifier, firstLevelSalt, salt, useV4Uid, false)), actualMap.getString("advertising_id")); + } else { + assertEquals(EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(identityType, expectedIdentifier, firstLevelSalt, salt, useV4Uid, false)), actualMap.getString("advertising_id")); + } } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); } @@ -533,49 +520,42 @@ private void checkIdentityMapResponseWithV4Uid(JsonObject response, SaltEntry sa } } - protected SaltEntry setupSalts() { - when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(rotatingSalt123); - return rotatingSalt123; + private void checkIdentityMapResponse(JsonObject response) { + checkIdentityMapResponse(response, null, false, null, false); } - protected SaltEntry setupSaltsForV4UidAndV4PrevUid() { - when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); + protected SaltEntry setupSalts(boolean useV4Uid, Boolean useV4PrevUid) { + return useV4Uid ? setupSaltsForV4Uid(useV4PrevUid) : setupSaltsForV2V3Uid(useV4PrevUid); + } - var lastUpdated = Instant.now().minus(1, DAYS); - var refreshFrom = lastUpdated.plus(30, DAYS); - SaltEntry salt = new SaltEntry( - 1, - "1", - lastUpdated.toEpochMilli(), - null, - refreshFrom.toEpochMilli(), - null, - new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), - new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); - return salt; + protected SaltEntry setupSalts(boolean useV4Uid) { + return setupSalts(useV4Uid, null); + } + + protected SaltEntry setupSalts() { + return setupSalts(false, null); } - protected SaltEntry setupSaltsForV4UidAndV3PrevUid() { + protected SaltEntry setupSaltsForV2V3Uid(Boolean useV4PrevUid) { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); var lastUpdated = Instant.now().minus(1, DAYS); var refreshFrom = lastUpdated.plus(30, DAYS); SaltEntry salt = new SaltEntry( - 1, - "1", + rotatingSalt123.id(), + rotatingSalt123.hashedId(), lastUpdated.toEpochMilli(), - null, + rotatingSalt123.currentSalt(), refreshFrom.toEpochMilli(), - "salt123", - new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), - null); + useV4PrevUid == null || useV4PrevUid ? null : rotatingSalt123.previousSalt(), + null, + useV4PrevUid == null || !useV4PrevUid ? null : new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235") + ); when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); return salt; } - protected SaltEntry setupSaltsForV4UidAndNoPrevUid() { + protected SaltEntry setupSaltsForV4Uid(Boolean useV4PrevUid) { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); var lastUpdated = Instant.now().minus(1, DAYS); @@ -586,9 +566,9 @@ protected SaltEntry setupSaltsForV4UidAndNoPrevUid() { lastUpdated.toEpochMilli(), null, refreshFrom.toEpochMilli(), - null, + useV4PrevUid == null || useV4PrevUid ? null : "salt123", new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), - null); + useV4PrevUid == null || !useV4PrevUid ? null : new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); return salt; } @@ -700,6 +680,14 @@ private void assertTokenStatusMetrics(Integer siteId, TokenResponseStatsCollecto assertEquals(1, actual); } + private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean useV4Uid, boolean usePrevUid) throws Exception { + if (useV4Uid) { + return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, usePrevUid ? salt.previousKeySalt() : salt.currentKeySalt()); + } else { + return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, usePrevUid ? salt.previousSalt() : salt.currentSalt()); + } + } + private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, String rotatingSalt) { return getRawUid(getIdentityScope(), identityType, identityString, firstLevelSalt, rotatingSalt, useRawUidV3()); } @@ -724,6 +712,14 @@ public static byte[] getRawUidV4(IdentityScope identityScope, IdentityType ident return TokenUtils.getAdvertisingIdV4FromIdentity(identityScope, identityType, identityEnvironment, identityString, firstLevelSalt, rotatingKey); } + private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean useV4Uid, boolean usePrevUid) throws Exception { + if (useV4Uid) { + return getAdvertisingIdFromIdentityHash(identityType, identityString, firstLevelSalt, usePrevUid ? salt.previousKeySalt() : salt.currentKeySalt()); + } else { + return getAdvertisingIdFromIdentityHash(identityType, identityString, firstLevelSalt, usePrevUid ? salt.previousSalt() : salt.currentSalt()); + } + } + private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, String rotatingSalt) { return !useRawUidV3() ? TokenUtils.getAdvertisingIdV2FromIdentityHash(identityString, firstLevelSalt, rotatingSalt) @@ -1158,34 +1154,7 @@ void v3IdentityMapMixedInputSuccess( final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - SaltEntry salt; - if (useV4Uid) { - if (useV4PrevUid == null) { - salt = setupSaltsForV4UidAndNoPrevUid(); - } else if (useV4PrevUid) { - salt = setupSaltsForV4UidAndV4PrevUid(); - } else { - salt = setupSaltsForV4UidAndV3PrevUid(); - } - } else { - var lastUpdated = Instant.now().minus(1, DAYS); - var refreshFrom = lastUpdated.plus(30, DAYS); - salt = setupSalts(); - - if (useV4PrevUid == null) { - salt = new SaltEntry( - salt.id(), salt.hashedId(), lastUpdated.toEpochMilli(), - salt.currentSalt(), refreshFrom.toEpochMilli(), null, null, null); - } else if (useV4PrevUid) { - salt = new SaltEntry( - salt.id(), salt.hashedId(), lastUpdated.toEpochMilli(), - salt.currentSalt(), refreshFrom.toEpochMilli(), null, null, new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); - } else { - salt = new SaltEntry( - salt.id(), salt.hashedId(), lastUpdated.toEpochMilli(), - salt.currentSalt(), refreshFrom.toEpochMilli(), salt.previousSalt(), null, null); - } - } + SaltEntry salt = setupSalts(useV4Uid, useV4PrevUid); when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); var phoneHash = TokenUtils.getIdentityHashString("+15555555555"); @@ -1198,7 +1167,6 @@ void v3IdentityMapMixedInputSuccess( """, phoneHash) ); - SaltEntry finalSalt = salt; send(vertx, "v3/identity/map", request, 200, respJson -> { JsonObject body = respJson.getJsonObject("body"); assertEquals(Set.of("email", "email_hash", "phone", "phone_hash"), body.fieldNames()); @@ -1213,119 +1181,23 @@ void v3IdentityMapMixedInputSuccess( JsonObject mappedPhoneHashExpected; try { - if (useV4Uid) { - if (useV4PrevUid == null) { - mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), - "p", null, - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), - "p", null, - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentKeySalt())), - "p", null, - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - } else if (useV4PrevUid) { - mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousKeySalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - } else { - mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousSalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousSalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentKeySalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousSalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - } - } else { - if (useV4PrevUid == null) { - mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentSalt())), - "p", null, - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentSalt())), - "p", null, - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentSalt())), - "p", null, - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - } else if (useV4PrevUid) { - mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousKeySalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousKeySalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - } else { - mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, finalSalt.previousSalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, finalSalt.previousSalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - - mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.currentSalt())), - "p", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, finalSalt.previousSalt())), - "r", Instant.ofEpochMilli(finalSalt.refreshFrom()).getEpochSecond() - ); - } - } + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt, useV4Uid, false)), + "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt, useV4PrevUid, true)), + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt, useV4Uid, false)), + "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt, useV4PrevUid, true)), + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt, useV4Uid, false)), + "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt, useV4PrevUid, true)), + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() + ); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); testContext.failNow(e); @@ -1348,12 +1220,7 @@ void v3IdentityMapMixedInputSuccess( void v3IdentityMapUnmappedIdentitiesOptoutAndInvalid(boolean useV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); // optout when(this.optOutStore.getLatestEntry(any())).thenReturn(Instant.now()); @@ -1775,7 +1642,7 @@ void tokenGenerateForEmail(boolean useV4Uid, String contentType, Vertx vertx, Ve fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry salt = setupSalts(useV4Uid); JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -1792,11 +1659,7 @@ void tokenGenerateForEmail(boolean useV4Uid, String contentType, Vertx vertx, Ve assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); try { - if (useV4Uid) { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); - } else { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); testContext.failNow(e); @@ -1880,7 +1743,7 @@ void tokenGenerateThenRefresh( fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry salt = setupSalts(useV4Uid); Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, HttpHeaders.CONTENT_TYPE.toString(), contentType); @@ -1892,11 +1755,7 @@ void tokenGenerateThenRefresh( AdvertisingToken advertisingToken = validateAndGetToken(encoder, bodyJson, IdentityType.Email); try { - if (useV4Uid) { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); - } else { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); testContext.failNow(e); @@ -1907,7 +1766,7 @@ void tokenGenerateThenRefresh( when(this.optOutStore.getLatestEntry(any())).thenReturn(null); - SaltEntry refreshSalt = useRefreshedV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry refreshSalt = setupSalts(useRefreshedV4Uid); sendTokenRefresh(vertx, testContext, genRefreshToken, bodyJson.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("success", refreshRespJson.getString("status")); JsonObject refreshBody = refreshRespJson.getJsonObject("body"); @@ -1919,11 +1778,7 @@ void tokenGenerateThenRefresh( assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, adTokenFromRefresh.publisherIdentity.siteId); try { - if (useRefreshedV4Uid) { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); - } else { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt.currentSalt()), adTokenFromRefresh.userIdentity.id); - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); testContext.failNow(e); @@ -2053,7 +1908,7 @@ void tokenGenerateThenValidateWithEmail_Match(boolean useV4Uid, Vertx vertx, Ver fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry salt = setupSalts(useV4Uid); generateTokens(vertx, "email", emailAddress, genRespJson -> { assertEquals("success", genRespJson.getString("status")); @@ -2063,11 +1918,7 @@ void tokenGenerateThenValidateWithEmail_Match(boolean useV4Uid, Vertx vertx, Ver String advertisingTokenString = genBody.getString("advertising_token"); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, IdentityType.Email); try { - if (useV4Uid) { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); - } else { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); testContext.failNow(e); @@ -2278,12 +2129,7 @@ void tokenRefreshInvalidTokenUnauthenticated(Vertx vertx, VertxTestContext testC private void generateRefreshToken(Vertx vertx, String identityType, String identity, int siteId, boolean useV4Uid, Handler handler) { fakeAuth(siteId, Role.GENERATOR); setupKeys(); - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); generateTokens(vertx, identityType, identity, handler); } @@ -2388,12 +2234,7 @@ void tokenRefreshExpiredTokenUnauthenticated(Vertx vertx, VertxTestContext testC void tokenRefreshOptOut(boolean useV4Uid, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; final String emailAddress = "test@uid2.com"; - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); generateRefreshToken(vertx, "email", emailAddress, clientSiteId, genRespJson -> { JsonObject bodyJson = genRespJson.getJsonObject("body"); @@ -2430,11 +2271,7 @@ void tokenRefreshOptOutBeforeLogin(boolean useV4Uid, boolean useRefreshedV4Uid, when(this.optOutStore.getLatestEntry(any())).thenReturn(now.minusSeconds(10)); - if (useRefreshedV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useRefreshedV4Uid); sendTokenRefresh(vertx, testContext, refreshToken, refreshTokenDecryptSecret, 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); @@ -2496,8 +2333,8 @@ void identityMapBatchBothEmailAndHashEmpty(Vertx vertx, VertxTestContext testCon req.put("email", emails); req.put("email_hash", emailHashes); - send(vertx, "v2/identity/map", req, 200, respJson -> { - checkIdentityMapResponse(respJson); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json); testContext.completeNow(); }); } @@ -2625,16 +2462,12 @@ void identityMapBatchEmails(boolean useV4Uid, Vertx vertx, VertxTestContext test fakeAuth(clientSiteId, Role.MAPPER); setupKeys(); - SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry salt = setupSalts(useV4Uid); JsonObject req = createBatchEmailsRequestPayload(); send(vertx, "v2/identity/map", req, 200, json -> { - if (useV4Uid) { - checkIdentityMapResponseWithV4Uid(json, salt, "test1@uid2.com", "test2@uid2.com"); - } else { - checkIdentityMapResponse(json, "test1@uid2.com", "test2@uid2.com"); - } + checkIdentityMapResponse(json, salt, useV4Uid, IdentityType.Email, false, "test1@uid2.com", "test2@uid2.com"); testContext.completeNow(); }); } @@ -2643,9 +2476,10 @@ void identityMapBatchEmails(boolean useV4Uid, Vertx vertx, VertxTestContext test void identityMapBatchEmailHashes(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray hashes = new JsonArray(); req.put("email_hash", hashes); @@ -2659,7 +2493,7 @@ void identityMapBatchEmailHashes(Vertx vertx, VertxTestContext testContext) { } send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, emailHashes); + checkIdentityMapResponse(json, salt, false, IdentityType.Email, true, emailHashes); testContext.completeNow(); }); } @@ -2668,9 +2502,10 @@ void identityMapBatchEmailHashes(Vertx vertx, VertxTestContext testContext) { void identityMapBatchEmailsOneEmailInvalid(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray emails = new JsonArray(); req.put("email", emails); @@ -2680,7 +2515,7 @@ void identityMapBatchEmailsOneEmailInvalid(Vertx vertx, VertxTestContext testCon emails.add("test2@uid2.com"); send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "test1@uid2.com", "test2@uid2.com"); + checkIdentityMapResponse(json, salt, false, IdentityType.Email, false, "test1@uid2.com", "test2@uid2.com"); testContext.completeNow(); }); } @@ -2754,12 +2589,7 @@ private static Stream optOutStatusRequestData() { void optOutStatusRequest(boolean useV4Uid, Map optedOutIds, int optedOutCount, Role role, Vertx vertx, VertxTestContext testContext) { fakeAuth(126, role); setupKeys(); - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); JsonArray rawUIDs = new JsonArray(); for (String rawUID2 : optedOutIds.keySet()) { @@ -2843,12 +2673,7 @@ void logoutV2(boolean useV4Uid, String contentType, Vertx vertx, VertxTestContex final int clientSiteId = 201; fakeAuth(clientSiteId, Role.OPTOUT); setupKeys(); - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); JsonObject req = new JsonObject(); req.put("email", "test@uid2.com"); @@ -3164,7 +2989,6 @@ void tokenGenerateThenValidateWithPhoneHash_Match(Vertx vertx, VertxTestContext @Test void tokenGenerateThenValidateWithBothPhoneAndPhoneHash(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; final String phone = ValidateIdentityForPhone; final String phoneHash = EncodingUtils.toBase64String(ValidateIdentityForEmailHash); fakeAuth(clientSiteId, Role.GENERATOR); @@ -3183,7 +3007,7 @@ void tokenGenerateThenValidateWithBothPhoneAndPhoneHash(Vertx vertx, VertxTestCo v2Payload.put("phone", phone); v2Payload.put("phone_hash", phoneHash); - send(vertx, apiVersion + "/token/validate", v2Payload, 400, json -> { + send(vertx, "v2/token/validate", v2Payload, 400, json -> { assertFalse(json.containsKey("body")); assertEquals("client_error", json.getString("status")); @@ -3205,8 +3029,8 @@ void identityMapBatchBothPhoneAndHashEmpty(Vertx vertx, VertxTestContext testCon req.put("phone", phones); req.put("phone_hash", phoneHashes); - send(vertx, "v2/identity/map", req, 200, respJson -> { - checkIdentityMapResponse(respJson); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json); testContext.completeNow(); }); } @@ -3238,9 +3062,10 @@ void identityMapBatchBothPhoneAndHashSpecified(Vertx vertx, VertxTestContext tes void identityMapBatchPhones(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray phones = new JsonArray(); req.put("phone", phones); @@ -3249,7 +3074,7 @@ void identityMapBatchPhones(Vertx vertx, VertxTestContext testContext) { phones.add("+15555555556"); send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "+15555555555", "+15555555556"); + checkIdentityMapResponse(json, salt, false, IdentityType.Phone, false, "+15555555555", "+15555555556"); testContext.completeNow(); }); } @@ -3258,23 +3083,24 @@ void identityMapBatchPhones(Vertx vertx, VertxTestContext testContext) { void identityMapBatchPhoneHashes(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray hashes = new JsonArray(); req.put("phone_hash", hashes); - final String[] emailHashes = { + final String[] phoneHashes = { TokenUtils.getIdentityHashString("+15555555555"), TokenUtils.getIdentityHashString("+15555555556"), }; - for (String emailHash : emailHashes) { - hashes.add(emailHash); + for (String phoneHash : phoneHashes) { + hashes.add(phoneHash); } send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, emailHashes); + checkIdentityMapResponse(json, salt, false, IdentityType.Phone, true, phoneHashes); testContext.completeNow(); }); } @@ -3283,9 +3109,10 @@ void identityMapBatchPhoneHashes(Vertx vertx, VertxTestContext testContext) { void identityMapBatchPhonesOnePhoneInvalid(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray phones = new JsonArray(); req.put("phone", phones); @@ -3295,7 +3122,7 @@ void identityMapBatchPhonesOnePhoneInvalid(Vertx vertx, VertxTestContext testCon phones.add("+15555555556"); send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "+15555555555", "+15555555556"); + checkIdentityMapResponse(json, salt, false, IdentityType.Phone, false, "+15555555555", "+15555555556"); testContext.completeNow(); }); } @@ -3348,12 +3175,7 @@ void tokenGenerateRespectOptOutOption(boolean useV4Uid, String policyParameterKe final int clientSiteId = 201; fakeAuth(clientSiteId, Role.GENERATOR); setupKeys(); - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) @@ -3384,12 +3206,7 @@ void identityMapOptoutDefaultOption(boolean useV4Uid, Vertx vertx, VertxTestCont final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); setupKeys(); - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) @@ -3427,12 +3244,7 @@ void identityMapRespectOptOutOption(boolean useV4Uid, String policyParameterKey, final int clientSiteId = 201; fakeAuth(clientSiteId, Role.MAPPER); setupKeys(); - - if (useV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useV4Uid); // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) @@ -3460,7 +3272,6 @@ void identityMapRespectOptOutOption(boolean useV4Uid, String policyParameterKey, @Test void requestWithoutClientKeyOrReferer(Vertx vertx, VertxTestContext testContext) { final String emailAddress = "test@uid2.com"; - final String apiVersion = "v2"; setupSalts(); setupKeys(); @@ -3471,7 +3282,7 @@ void requestWithoutClientKeyOrReferer(Vertx vertx, VertxTestContext testContext) json -> { assertEquals("unauthorized", json.getString("status")); - assertStatsCollector("/" + apiVersion + "/token/generate", null, null, null); + assertStatsCollector("/v2/token/generate", null, null, null); testContext.completeNow(); }); @@ -4338,7 +4149,7 @@ void cstgUserOptsOutAfterTokenGenerate( Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); - SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry salt = setupSalts(useV4Uid); final Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); @@ -4365,21 +4176,13 @@ void cstgUserOptsOutAfterTokenGenerate( final AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); final RefreshToken refreshToken = decodeRefreshToken(encoder, decodeV2RefreshToken(response), identityType); - if (useV4Uid) { - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt.currentKeySalt(), false); - } else { - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); - } + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt, false, useV4Uid, false); // When we refresh the token the user has opted out. when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(advertisingToken.userIdentity.establishedAt.plusSeconds(1)); - if (useRefreshedV4Uid) { - setupSaltsForV4UidAndV4PrevUid(); - } else { - setupSalts(); - } + setupSalts(useRefreshedV4Uid); sendTokenRefresh(vertx, testContext, genBody.getString("refresh_token"), genBody.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); @@ -4424,7 +4227,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( Vertx vertx, VertxTestContext testContext) throws NoSuchAlgorithmException, InvalidKeyException { setupCstgBackend("cstg.co.uk"); - SaltEntry salt = useV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry salt = setupSalts(useV4Uid); Tuple.Tuple2 data = createClientSideTokenGenerateRequest(identityType, id, Instant.now().toEpochMilli()); @@ -4457,11 +4260,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); try { - if (useV4Uid) { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentKeySalt()), advertisingToken.userIdentity.id); - } else { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt.currentSalt()), advertisingToken.userIdentity.id); - } + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); testContext.failNow(e); @@ -4470,11 +4269,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); - if (useV4Uid) { - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt.currentKeySalt(), false); - } else { - assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id); - } + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt, false, useV4Uid, false); assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("identity_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(genBody.getLong("refresh_from")), 10); @@ -4485,7 +4280,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( TokenResponseStatsCollector.ResponseStatus.Success, TokenResponseStatsCollector.PlatformType.HasOriginHeader); - SaltEntry refreshSalt = useRefreshedV4Uid ? setupSaltsForV4UidAndV4PrevUid() : setupSalts(); + SaltEntry refreshSalt = setupSalts(useRefreshedV4Uid); String genRefreshToken = genBody.getString("refresh_token"); //test a subsequent refresh from this cstg call and see if it still works sendTokenRefresh(vertx, testContext, genRefreshToken, genBody.getString("refresh_response_key"), 200, refreshRespJson -> { @@ -4496,11 +4291,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( //make sure the new advertising token from refresh looks right AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); try { - if (useRefreshedV4Uid) { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt.currentKeySalt()), adTokenFromRefresh.userIdentity.id); - } else { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt.currentSalt()), adTokenFromRefresh.userIdentity.id); - } + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); } catch (Exception e) { org.junit.jupiter.api.Assertions.fail(e.getMessage()); testContext.failNow(e); @@ -4511,11 +4302,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( assertNotEquals(genRefreshToken, refreshTokenStringNew); RefreshToken refreshTokenAfterRefresh = decodeRefreshToken(encoder, refreshTokenStringNew, identityType); - if (useRefreshedV4Uid) { - assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id, refreshSalt.currentKeySalt(), false); - } else { - assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id); - } + assertAreClientSideGeneratedTokens(adTokenFromRefresh, refreshTokenAfterRefresh, clientSideTokenGenerateSiteId, identityType, id, refreshSalt, false, useRefreshedV4Uid, false); assertEqualsClose(Instant.now().plusMillis(identityExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("identity_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshExpiresAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_expires")), 10); assertEqualsClose(Instant.now().plusMillis(refreshIdentityAfter.toMillis()), Instant.ofEpochMilli(refreshBody.getLong("refresh_from")), 10); @@ -4612,30 +4399,28 @@ void cstgInvalidInput(String identityType, String rawUID, Vertx vertx, VertxTest }); } - private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToken, RefreshToken refreshToken, int siteId, IdentityType identityType, String identity) { - assertAreClientSideGeneratedTokens(advertisingToken, - refreshToken, - siteId, - identityType, - identity, - null, - false); + private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToken, RefreshToken refreshToken, int siteId, IdentityType identityType, String identityString, SaltEntry salt, boolean expectedOptOut, boolean useV4Uid, boolean usePrevUid) { + if (useV4Uid) { + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, siteId, identityType, identityString, usePrevUid ? salt.previousKeySalt() : salt.currentKeySalt(), expectedOptOut); + } else { + assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, siteId, identityType, identityString); + } } - private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToken, RefreshToken refreshToken, int siteId, IdentityType identityType, String identity, SaltEntry.KeyMaterial key, boolean expectedOptOut) { + private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToken, RefreshToken refreshToken, int siteId, IdentityType identityType, String identityString, SaltEntry.KeyMaterial key, boolean expectedOptOut) { final PrivacyBits advertisingTokenPrivacyBits = PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits); final PrivacyBits refreshTokenPrivacyBits = PrivacyBits.fromInt(refreshToken.userIdentity.privacyBits); final byte[] advertisingId; if (key == null) { advertisingId = getAdvertisingIdFromIdentity(identityType, - identity, + identityString, firstLevelSalt, rotatingSalt123.currentSalt()); } else { try { advertisingId = getAdvertisingIdFromIdentity(identityType, - identity, + identityString, firstLevelSalt, key); } catch (Exception e) { @@ -4644,7 +4429,7 @@ private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToke } } - final byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity(identity, firstLevelSalt); + final byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity(identityString, firstLevelSalt); assertAll( () -> assertTrue(advertisingTokenPrivacyBits.isClientSideTokenGenerated(), "Advertising token privacy bits CSTG flag is incorrect"), @@ -4661,6 +4446,16 @@ private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToke ); } + private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToken, RefreshToken refreshToken, int siteId, IdentityType identityType, String identityString) { + assertAreClientSideGeneratedTokens(advertisingToken, + refreshToken, + siteId, + identityType, + identityString, + null, + false); + } + /******************************************************** * MULTIPLE-KEYSETS TESTS: KEY SHARING & TOKEN GENERATE * ********************************************************/ @@ -4937,7 +4732,6 @@ void keySharingKeysets_CorrectFiltering(String contentType, Vertx vertx, VertxTe // The master key -2 // The publisher General 2 // Any other key without an ACL - String apiVersion = "v2"; int siteId = 4; fakeAuth(siteId, Role.SHARER); Keyset[] keysets = { @@ -4969,7 +4763,7 @@ void keySharingKeysets_CorrectFiltering(String contentType, Vertx vertx, VertxTe KeysetKey[] expectedKeys = new KeysetKey[]{masterKey, clientsKey, sharingkey12, sharingkey13, sharingkey14}; Arrays.sort(expectedKeys, Comparator.comparing(KeysetKey::getId)); - send(vertx, apiVersion + "/key/sharing", null, 200, respJson -> { + send(vertx, "v2/key/sharing", null, 200, respJson -> { System.out.println(respJson); checkEncryptionKeys(respJson, KeyDownloadEndpoint.SHARING, siteId, expectedKeys); testContext.completeNow(); @@ -5048,7 +4842,6 @@ public void setupConfig() { @Test public void keyBidstreamReturnsCustomMaxBidstreamLifetimeHeader(Vertx vertx, VertxTestContext testContext) { - final String apiVersion = "v2"; final KeyDownloadEndpoint endpoint = KeyDownloadEndpoint.BIDSTREAM; final int clientSiteId = 101; @@ -5057,7 +4850,7 @@ public void keyBidstreamReturnsCustomMaxBidstreamLifetimeHeader(Vertx vertx, Ver // Required, sets up mock keys. new MultipleKeysetsTests(); - send(vertx, apiVersion + endpoint.getPath(), null, 200, respJson -> { + send(vertx, "v2" + endpoint.getPath(), null, 200, respJson -> { assertEquals("success", respJson.getString("status")); checkKeyDownloadResponseHeaderFields(endpoint, respJson.getJsonObject("body"), clientSiteId); @@ -5115,7 +4908,6 @@ void keyDownloadEndpointKeysets_IDREADER(boolean provideAppNames, KeyDownloadEnd if (!provideAppNames) { this.uidOperatorVerticle.setKeySharingEndpointProvideAppNames(false); } - String apiVersion = "v2"; int clientSiteId = 101; fakeAuth(clientSiteId, Role.ID_READER); MultipleKeysetsTests test = new MultipleKeysetsTests(); @@ -5152,7 +4944,7 @@ void keyDownloadEndpointKeysets_IDREADER(boolean provideAppNames, KeyDownloadEnd doReturn(new Site(104, "site104", true, new HashSet<>())).when(siteProvider).getSite(104); Arrays.sort(expectedKeys, Comparator.comparing(KeysetKey::getId)); - send(vertx, apiVersion + endpoint.getPath(), null, 200, respJson -> { + send(vertx, "v2" + endpoint.getPath(), null, 200, respJson -> { System.out.println(respJson); assertEquals("success", respJson.getString("status")); @@ -5197,7 +4989,6 @@ void keySharingKeysets_SHARER(boolean provideSiteDomainNames, boolean provideApp if (!provideAppNames) { this.uidOperatorVerticle.setKeySharingEndpointProvideAppNames(false); } - String apiVersion = "v2"; int clientSiteId = 101; fakeAuth(clientSiteId, Role.SHARER); MultipleKeysetsTests test = new MultipleKeysetsTests(); @@ -5224,7 +5015,7 @@ void keySharingKeysets_SHARER(boolean provideSiteDomainNames, boolean provideApp }; Arrays.sort(expectedKeys, Comparator.comparing(KeysetKey::getId)); - send(vertx, apiVersion + "/key/sharing", null, 200, respJson -> { + send(vertx, "v2/key/sharing", null, 200, respJson -> { System.out.println(respJson); assertEquals("success", respJson.getString("status")); assertEquals(clientSiteId, respJson.getJsonObject("body").getInteger("caller_site_id")); @@ -5247,7 +5038,6 @@ void keySharingKeysets_SHARER(boolean provideSiteDomainNames, boolean provideApp @Test void keySharingKeysets_ReturnsMasterAndSite(Vertx vertx, VertxTestContext testContext) { - String apiVersion = "v2"; int siteId = 5; fakeAuth(siteId, Role.SHARER); Keyset[] keysets = { @@ -5261,7 +5051,7 @@ void keySharingKeysets_ReturnsMasterAndSite(Vertx vertx, VertxTestContext testCo MultipleKeysetsTests test = new MultipleKeysetsTests(Arrays.asList(keysets), Arrays.asList(encryptionKeys)); setupSiteDomainAndAppNameMock(true, false, 101, 102, 103, 104, 105); Arrays.sort(encryptionKeys, Comparator.comparing(KeysetKey::getId)); - send(vertx, apiVersion + "/key/sharing", null, 200, respJson -> { + send(vertx, "v2/key/sharing", null, 200, respJson -> { System.out.println(respJson); verifyExpectedSiteDetail(new HashMap<>(), respJson.getJsonObject("body").getJsonArray("site_data")); checkEncryptionKeys(respJson, KeyDownloadEndpoint.SHARING, siteId, encryptionKeys); @@ -5272,7 +5062,6 @@ void keySharingKeysets_ReturnsMasterAndSite(Vertx vertx, VertxTestContext testCo @ParameterizedTest @ValueSource(strings = {"NoKeyset", "NoKey", "SharedKey"}) void keySharingKeysets_CorrectIDS(String testRun, Vertx vertx, VertxTestContext testContext) { - String apiVersion = "v2"; int siteId = 0; KeysetKey[] keys = null; @@ -5313,7 +5102,7 @@ void keySharingKeysets_CorrectIDS(String testRun, Vertx vertx, VertxTestContext final KeysetKey[] expectedKeys = Arrays.copyOfRange(keys, 0, keys.length); Arrays.sort(expectedKeys, Comparator.comparing(KeysetKey::getId)); - send(vertx, apiVersion + "/key/sharing", null, 200, respJson -> { + send(vertx, "v2/key/sharing", null, 200, respJson -> { System.out.println(respJson); assertEquals(clientSiteId, respJson.getJsonObject("body").getInteger("caller_site_id")); assertEquals(UIDOperatorVerticle.MASTER_KEYSET_ID_FOR_SDKS, respJson.getJsonObject("body").getInteger("master_keyset_id")); @@ -5367,7 +5156,6 @@ private static List keyDownloadEndpointRotatingKeysets_IDREADER_sourc // ID_READER has no access to a keyset with an empty allowed_sites - reject by sharing // ID_READER has no access to a keyset with an allowed_sites for other sites - reject by sharing void keyDownloadEndpointRotatingKeysets_IDREADER(String testRun, KeyDownloadEndpoint endpoint, Vertx vertx, VertxTestContext testContext) { - String apiVersion = "v2"; int clientSiteId = 101; fakeAuth(clientSiteId, Role.ID_READER); MultipleKeysetsTests test = new MultipleKeysetsTests(); @@ -5456,7 +5244,7 @@ void keyDownloadEndpointRotatingKeysets_IDREADER(String testRun, KeyDownloadEndp // test and validate results expectedKeys.sort(Comparator.comparing(KeysetKey::getId)); - send(vertx, apiVersion + endpoint.getPath(), null, 200, respJson -> { + send(vertx, "v2" + endpoint.getPath(), null, 200, respJson -> { System.out.println(respJson); assertEquals("success", respJson.getString("status")); final JsonObject body = respJson.getJsonObject("body"); @@ -5496,11 +5284,13 @@ private void checkKeyDownloadResponseHeaderFields(KeyDownloadEndpoint endpoint, @Test void secureLinkValidationPassesReturnsIdentity(Vertx vertx, VertxTestContext testContext) { + SaltEntry salt = setupSalts(); + JsonObject req = setupIdentityMapServiceLinkTest(); when(this.secureLinkValidatorService.validateRequest(any(RoutingContext.class), any(JsonObject.class), any(Role.class))).thenReturn(true); - send(vertx, "v2" + "/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "test1@uid2.com", "test2@uid2.com"); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json, salt, false, IdentityType.Email, false,"test1@uid2.com", "test2@uid2.com"); testContext.completeNow(); }); } @@ -5510,7 +5300,7 @@ void secureLinkValidationFailsReturnsIdentityError(Vertx vertx, VertxTestContext JsonObject req = setupIdentityMapServiceLinkTest(); when(this.secureLinkValidatorService.validateRequest(any(RoutingContext.class), any(JsonObject.class), any(Role.class))).thenReturn(false); - send(vertx, "v2" + "/identity/map", req, 401, json -> { + send(vertx, "v2/identity/map", req, 401, json -> { assertEquals("unauthorized", json.getString("status")); assertEquals("Invalid link_id", json.getString("message")); testContext.completeNow(); @@ -5563,7 +5353,6 @@ void keySharingRespectsConfigValues(Vertx vertx, VertxTestContext testContext) { .withMaxBidstreamLifetimeSeconds(newMaxSharingLifetimeSeconds) .build(); - String apiVersion = "v2"; int siteId = 5; fakeAuth(siteId, Role.SHARER); Keyset[] keysets = { @@ -5576,7 +5365,7 @@ void keySharingRespectsConfigValues(Vertx vertx, VertxTestContext testContext) { }; MultipleKeysetsTests test = new MultipleKeysetsTests(Arrays.asList(keysets), Arrays.asList(encryptionKeys)); setupSiteDomainAndAppNameMock(true, false, 101, 102, 103, 104, 105); - send(vertx, apiVersion + "/key/sharing", null, 200, respJson -> { + send(vertx, "v2/key/sharing", null, 200, respJson -> { testContext.verify(() -> { JsonObject body = respJson.getJsonObject("body"); assertNotNull(body); @@ -5597,7 +5386,6 @@ void keyBidstreamRespectsConfigValues(String contentType, Vertx vertx, VertxTest .withMaxBidstreamLifetimeSeconds(newMaxBidstreamLifetimeSeconds) .build(); - final String apiVersion = "v2"; final KeyDownloadEndpoint endpoint = KeyDownloadEndpoint.BIDSTREAM; final int clientSiteId = 101; @@ -5606,7 +5394,7 @@ void keyBidstreamRespectsConfigValues(String contentType, Vertx vertx, VertxTest // Required, sets up mock keys. new MultipleKeysetsTests(); - send(vertx, apiVersion + endpoint.getPath(), null, 200, respJson -> { + send(vertx, "v2" + endpoint.getPath(), null, 200, respJson -> { testContext.verify(() -> { JsonObject body = respJson.getJsonObject("body"); assertNotNull(body); @@ -5662,7 +5450,6 @@ void keySharingRespectsConfigValuesWithRemoteConfig(Vertx vertx, VertxTestContex .withMaxSharingLifetimeSeconds(newMaxSharingLifetimeSeconds) .build(); - String apiVersion = "v2"; int siteId = 5; fakeAuth(siteId, Role.SHARER); Keyset[] keysets = { @@ -5675,7 +5462,7 @@ void keySharingRespectsConfigValuesWithRemoteConfig(Vertx vertx, VertxTestContex }; MultipleKeysetsTests test = new MultipleKeysetsTests(Arrays.asList(keysets), Arrays.asList(encryptionKeys)); setupSiteDomainAndAppNameMock(true, false, 101, 102, 103, 104, 105); - send(vertx, apiVersion + "/key/sharing", null, 200, respJson -> { + send(vertx, "v2/key/sharing", null, 200, respJson -> { testContext.verify(() -> { JsonObject body = respJson.getJsonObject("body"); assertNotNull(body); @@ -5695,7 +5482,6 @@ void keyBidstreamRespectsConfigValuesWithRemoteConfig(Vertx vertx, VertxTestCont .withMaxBidstreamLifetimeSeconds(newMaxBidstreamLifetimeSeconds) .build(); - final String apiVersion = "v2"; final KeyDownloadEndpoint endpoint = KeyDownloadEndpoint.BIDSTREAM; final int clientSiteId = 101; @@ -5704,7 +5490,7 @@ void keyBidstreamRespectsConfigValuesWithRemoteConfig(Vertx vertx, VertxTestCont // Required, sets up mock keys. new MultipleKeysetsTests(); - send(vertx, apiVersion + endpoint.getPath(), null, 200, respJson -> { + send(vertx, "v2" + endpoint.getPath(), null, 200, respJson -> { testContext.verify(() -> { JsonObject body = respJson.getJsonObject("body"); assertNotNull(body); From ba79783ec3e12969ef4d8ab95bf910d0fa59cbf6 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Wed, 17 Sep 2025 15:07:37 +0800 Subject: [PATCH 25/38] Base64-encoded last 16 bytes of FLH for v4 UID --- src/main/java/com/uid2/operator/service/V4TokenUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java index b59f0ecad..355d1bd69 100644 --- a/src/main/java/com/uid2/operator/service/V4TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -36,7 +36,7 @@ public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte metadata, int keyId) { String ivBase = salt - .concat(Arrays.toString(firstLevelHashLast16Bytes)) + .concat(EncodingUtils.toBase64String(firstLevelHashLast16Bytes)) .concat(Byte.toString(metadata)) .concat(String.valueOf(keyId)); return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(ivBase), 0, IV_LENGTH); From 15e78201140650fc7520f51e157b5f5d8dafa477 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 18 Sep 2025 13:31:52 +0800 Subject: [PATCH 26/38] Updated v4 UID IV generation --- .../uid2/operator/service/EncodingUtils.java | 64 ++++++++----------- .../uid2/operator/service/V4TokenUtils.java | 14 ++-- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/EncodingUtils.java b/src/main/java/com/uid2/operator/service/EncodingUtils.java index c27d11db3..a8d5fcb7b 100644 --- a/src/main/java/com/uid2/operator/service/EncodingUtils.java +++ b/src/main/java/com/uid2/operator/service/EncodingUtils.java @@ -8,7 +8,9 @@ import java.util.Base64; import java.util.UUID; -public class EncodingUtils { +public final class EncodingUtils { + private EncodingUtils() { + } public static String toBase64String(byte[] b) { return Base64.getEncoder().encodeToString(b); @@ -18,9 +20,13 @@ public static byte[] toBase64(byte[] b) { return Base64.getEncoder().encode(b); } - public static byte[] fromBase64(String s) { return Base64.getDecoder().decode(s); } + public static byte[] fromBase64(String s) { + return Base64.getDecoder().decode(s); + } - public static byte[] fromBase64(byte[] b) { return Base64.getDecoder().decode(b); } + public static byte[] fromBase64(byte[] b) { + return Base64.getDecoder().decode(b); + } public static String getSha256(String input, String salt) { return toBase64String(getSha256Bytes(input, salt)); @@ -34,10 +40,18 @@ public static byte[] getSha256Bytes(String input) { return getSha256Bytes(input, null); } + public static byte[] getSha256Bytes(byte[] input) { + return getSha256Bytes(input, null); + } + public static byte[] getSha256Bytes(String input, String salt) { + return getSha256Bytes(input.getBytes(), salt); + } + + public static byte[] getSha256Bytes(byte[] input, String salt) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); - md.update(input.getBytes()); + md.update(input); if (salt != null) { md.update(salt.getBytes()); } @@ -47,34 +61,6 @@ public static byte[] getSha256Bytes(String input, String salt) { } } - public static String generateIdGuid(String value) { - byte[] b = value.getBytes(Charset.forName("UTF8")); - long high = 0L; - high = high | (long) b[0] << (7 * 8); - high = high | (long) b[1] << (6 * 8); - high = high | (long) b[2] << (5 * 8); - high = high | (long) b[3] << (5 * 8); - high = high | (long) b[4] << (3 * 8); - high = high | (long) b[5] << (2 * 8); - high = high | (long) b[6] << (1 * 8); - high = high | (long) b[7]; - - long low = 0; - low = low | (long) b[8] << (7 * 8); - low = low | (long) b[9] << (6 * 8); - low = low | (long) b[10] << (5 * 8); - low = low | (long) b[11] << (4 * 8); - low = low | (long) b[12] << (3 * 8); - low = low | (long) b[13] << (2 * 8); - low = low | (long) b[14] << (1 * 8); - low = low | (long) b[15]; - - String uid = new UUID(high, low).toString(); - - return uid; - - } - public static Instant NowUTCMillis() { return Instant.now().truncatedTo(ChronoUnit.MILLIS); } @@ -84,21 +70,21 @@ public static Instant NowUTCMillis(Clock clock) { } public static byte[] fromHexString(String hs) throws NumberFormatException { - if(hs.length() % 2 == 1) { + if (hs.length() % 2 == 1) { throw new NumberFormatException("input " + hs.substring(0, 5) + "... is not a valid hex string - odd length"); } byte[] s = new byte[hs.length() / 2]; - for(int i = 0; i < hs.length(); i++) { + for (int i = 0; i < hs.length(); i++) { int v; char c = hs.charAt(i); - if(c >= '0' && c <= '9') v = c - '0'; - else if(c >= 'A' && c <= 'F') v = c - 'A' + 10; - else if(c >= 'a' && c <= 'f') v = c - 'a' + 10; + if (c >= '0' && c <= '9') v = c - '0'; + else if (c >= 'A' && c <= 'F') v = c - 'A' + 10; + else if (c >= 'a' && c <= 'f') v = c - 'a' + 10; else throw new NumberFormatException("input " + hs.substring(0, 5) + "... is not a valid hex string - invalid character"); if (i % 2 == 0) { - s[i / 2] = (byte) (s[i / 2] | (byte)((v << 4) & 0xFF)); + s[i / 2] = (byte) (s[i / 2] | (byte) ((v << 4) & 0xFF)); } else { - s[i / 2] = (byte) (s[i / 2] | (byte)(v & 0xFF)); + s[i / 2] = (byte) (s[i / 2] | (byte) (v & 0xFF)); } } diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java index 355d1bd69..dbcfad026 100644 --- a/src/main/java/com/uid2/operator/service/V4TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -5,6 +5,7 @@ import javax.crypto.spec.SecretKeySpec; import io.vertx.core.buffer.Buffer; +import java.io.ByteArrayOutputStream; import java.util.Arrays; public final class V4TokenUtils { @@ -34,12 +35,13 @@ public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, return buffer.getBytes(); } - public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte metadata, int keyId) { - String ivBase = salt - .concat(EncodingUtils.toBase64String(firstLevelHashLast16Bytes)) - .concat(Byte.toString(metadata)) - .concat(String.valueOf(keyId)); - return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(ivBase), 0, IV_LENGTH); + public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte metadata, int keyId) throws Exception { + ByteArrayOutputStream ivBase = new ByteArrayOutputStream(); + ivBase.write(salt.getBytes()); + ivBase.write(firstLevelHashLast16Bytes); + ivBase.write(metadata); + ivBase.write(keyId); + return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(ivBase.toByteArray()), 0, IV_LENGTH); } public static byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception { From fa29edd9e8bb2700587a6b41f63881ddce3cad79 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 19 Sep 2025 00:12:36 +0000 Subject: [PATCH 27/38] [CI Pipeline] Released Snapshot version: 5.58.10-alpha-222-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d5dc3a4a4..a56eb6a0e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.58.9 + 5.58.10-alpha-222-SNAPSHOT UTF-8 From 14c260adefc8263e30fdfcf86f22a2bf09716a7b Mon Sep 17 00:00:00 2001 From: sophia chen Date: Mon, 22 Sep 2025 14:09:13 +1000 Subject: [PATCH 28/38] writing key id as big endian --- .../com/uid2/operator/service/V4TokenUtils.java | 17 ++++++++++------- .../uid2/operator/service/V4TokenUtilsTest.java | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java index dbcfad026..b6f7c881e 100644 --- a/src/main/java/com/uid2/operator/service/V4TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -4,7 +4,6 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import io.vertx.core.buffer.Buffer; - import java.io.ByteArrayOutputStream; import java.util.Arrays; @@ -14,6 +13,14 @@ public final class V4TokenUtils { private V4TokenUtils() { } + private static byte[] getKeyIdBytes(int keyId) { + return new byte[] { + (byte) ((keyId >> 16) & 0xFF), // MSB + (byte) ((keyId >> 8) & 0xFF), // Middle + (byte) (keyId & 0xFF), // LSB + }; + } + public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, int keyId, String key, String salt) throws Exception { byte[] firstLevelHashLast16Bytes = Arrays.copyOfRange(firstLevelHash, firstLevelHash.length - 16, firstLevelHash.length); byte[] iv = V4TokenUtils.generateIV(salt, firstLevelHashLast16Bytes, metadata, keyId); @@ -21,11 +28,7 @@ public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, Buffer buffer = Buffer.buffer(); buffer.appendByte(metadata); - buffer.appendBytes(new byte[] { - (byte) (keyId & 0xFF), // LSB - (byte) ((keyId >> 8) & 0xFF), // Middle - (byte) ((keyId >> 16) & 0xFF) // MSB - }); + buffer.appendBytes(getKeyIdBytes(keyId)); buffer.appendBytes(iv); buffer.appendBytes(encryptedFirstLevelHash); @@ -40,7 +43,7 @@ public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, b ivBase.write(salt.getBytes()); ivBase.write(firstLevelHashLast16Bytes); ivBase.write(metadata); - ivBase.write(keyId); + ivBase.write(getKeyIdBytes(keyId)); return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(ivBase.toByteArray()), 0, IV_LENGTH); } diff --git a/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java b/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java index c7f060a5a..e9fc3b396 100644 --- a/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java +++ b/src/test/java/com/uid2/operator/service/V4TokenUtilsTest.java @@ -28,7 +28,7 @@ void testBuildAdvertisingIdV4() throws Exception { byte extractedMetadata = v4UID[0]; byte[] keyIdBytes = Arrays.copyOfRange(v4UID, 1, 4); - int extractedKeyId = (keyIdBytes[0] & 0xFF) | ((keyIdBytes[1] & 0xFF) << 8) | ((keyIdBytes[2] & 0xFF) << 16); + int extractedKeyId = ((keyIdBytes[0] & 0xFF) << 16) | ((keyIdBytes[1] & 0xFF) << 8) | (keyIdBytes[2] & 0xFF); byte[] extractedIV = Arrays.copyOfRange(v4UID, 4, 16); byte[] extractedEncryptedHash = Arrays.copyOfRange(v4UID, 16, 32); byte extractedChecksum = v4UID[32]; From dcf4124817def5e74691bd73db4251c78233c4bc Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 26 Sep 2025 07:19:24 +0000 Subject: [PATCH 29/38] [CI Pipeline] Released Snapshot version: 5.58.38-alpha-231-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 51d40d8bd..1980d43f3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.58.37 + 5.58.38-alpha-231-SNAPSHOT UTF-8 From 439045f227c4322b03d442ffc9d62fdde8e88abc Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 26 Sep 2025 10:15:52 +0000 Subject: [PATCH 30/38] [CI Pipeline] Released Snapshot version: 5.58.46-alpha-233-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1919a3995..722848975 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.58.45 + 5.58.46-alpha-233-SNAPSHOT UTF-8 From a63e1fbf386a48301073bec7aef90bcf00911580 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 30 Sep 2025 13:26:10 +0800 Subject: [PATCH 31/38] Updated var naming and Cipher to ThreadLocal --- .../java/com/uid2/operator/service/V4TokenUtils.java | 10 +++++++++- .../com/uid2/operator/UIDOperatorVerticleTest.java | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java index b6f7c881e..c58091f4c 100644 --- a/src/main/java/com/uid2/operator/service/V4TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -5,9 +5,17 @@ import javax.crypto.spec.SecretKeySpec; import io.vertx.core.buffer.Buffer; import java.io.ByteArrayOutputStream; +import java.security.GeneralSecurityException; import java.util.Arrays; public final class V4TokenUtils { + private static final ThreadLocal CIPHER = ThreadLocal.withInitial(() -> { + try { + return Cipher.getInstance("AES/CTR/NoPadding"); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + }); private static final int IV_LENGTH = 12; private V4TokenUtils() { @@ -49,7 +57,7 @@ public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, b public static byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception { // Set up AES256-CTR cipher - Cipher aesCtr = Cipher.getInstance("AES/CTR/NoPadding"); + Cipher aesCtr = CIPHER.get(); SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(padIV16Bytes(iv)); diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 51a4b763b..98ee86643 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -680,11 +680,11 @@ private void assertTokenStatusMetrics(Integer siteId, TokenResponseStatsCollecto assertEquals(1, actual); } - private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean useV4Uid, boolean usePrevUid) throws Exception { - if (useV4Uid) { - return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, usePrevUid ? salt.previousKeySalt() : salt.currentKeySalt()); + private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean getV4Uid, boolean getPrevUid) throws Exception { + if (getV4Uid) { + return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, getPrevUid ? salt.previousKeySalt() : salt.currentKeySalt()); } else { - return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, usePrevUid ? salt.previousSalt() : salt.currentSalt()); + return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, getPrevUid ? salt.previousSalt() : salt.currentSalt()); } } From bd1c66e664c47d4169978d8a6c77b0808147e300 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Tue, 30 Sep 2025 17:40:07 +0800 Subject: [PATCH 32/38] Updated .trivyignore --- .trivyignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.trivyignore b/.trivyignore index bf9ee2050..55c6fbd29 100644 --- a/.trivyignore +++ b/.trivyignore @@ -10,3 +10,6 @@ CVE-2025-6965 exp:2025-10-01 # UID2-6097 CVE-2025-59375 exp:2025-12-15 + +# UID2-6128 +CVE-2025-55163 exp:2025-11-30 From 97e428a1995c699205346209d70fd45f53d94b94 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Tue, 30 Sep 2025 14:51:49 +0000 Subject: [PATCH 33/38] [CI Pipeline] Released Snapshot version: 5.58.51-alpha-236-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f22778504..67a8f1ba3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.58.50 + 5.58.51-alpha-236-SNAPSHOT UTF-8 From be44cf4958ba3bf81c29bee037be2f22fffbc88e Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 3 Oct 2025 10:23:51 +0000 Subject: [PATCH 34/38] [CI Pipeline] Released Snapshot version: 5.58.54-alpha-239-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 828aff65f..97e6c35f5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.58.53 + 5.58.54-alpha-239-SNAPSHOT UTF-8 From aca3947c4ca2fc9936df44ff13934a9024d16600 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Wed, 8 Oct 2025 16:16:39 +0800 Subject: [PATCH 35/38] Added raw UID version counter --- .../operator/service/UIDOperatorService.java | 30 +++++++++++++++---- .../operator/vertx/UIDOperatorVerticle.java | 1 - 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index be44950bc..fad7e2902 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -7,6 +7,8 @@ import com.uid2.operator.store.IOptOutStore; import com.uid2.shared.store.salt.ISaltProvider; import com.uid2.shared.model.TokenVersion; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Metrics; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; @@ -26,6 +28,7 @@ public class UIDOperatorService implements IUIDOperatorService { public static final Logger LOGGER = LoggerFactory.getLogger(UIDOperatorService.class); + public static final String IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS = "identity_token_expires_after_seconds"; public static final String REFRESH_TOKEN_EXPIRES_AFTER_SECONDS = "refresh_token_expires_after_seconds"; public static final String REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = "refresh_identity_token_after_seconds"; @@ -232,18 +235,35 @@ private MappedIdentity getMappedIdentity(UserIdentity firstLevelHashIdentity, In } private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt, SaltEntry.KeyMaterial key, IdentityEnvironment env) { + byte[] advertisingId; + if (salt != null) { - return rawUidV3Enabled - ? TokenUtils.getAdvertisingIdV3(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.id, salt) - : TokenUtils.getAdvertisingIdV2(firstLevelHashIdentity.id, salt); + if (rawUidV3Enabled) { + advertisingId = TokenUtils.getAdvertisingIdV3(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.id, salt); + incrementAdvertisingIdVersionCounter("v3"); + } else { + advertisingId = TokenUtils.getAdvertisingIdV2(firstLevelHashIdentity.id, salt); + incrementAdvertisingIdVersionCounter("v2"); + } } else { try { - return TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, env, firstLevelHashIdentity.id, key); + advertisingId = TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, env, firstLevelHashIdentity.id, key); + incrementAdvertisingIdVersionCounter("v4"); } catch (Exception e) { LOGGER.error("Exception when generating V4 advertising ID", e); - return null; + advertisingId = null; } } + + return advertisingId; + } + + private void incrementAdvertisingIdVersionCounter(String version) { + Counter.builder("uid2_raw_uid_version_total") + .description("counter for raw UID version") + .tag("version", version) + .register(Metrics.globalRegistry) + .increment(); } private byte[] getPreviousAdvertisingId(UserIdentity firstLevelHashIdentity, SaltEntry rotatingSalt, Instant asOf, IdentityEnvironment env) { diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 232ca48b2..439cb3c46 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -1462,7 +1462,6 @@ private void recordRefreshTokenVersionCount(String siteId, TokenVersion tokenVer .tags("site_id", siteId) .tags("refresh_token_version", tokenVersion.toString().toLowerCase()) .register(Metrics.globalRegistry).increment(); - } private RefreshResponse refreshIdentity(RoutingContext rc, String tokenStr) { From 5200af5bdeff7776f2462992a3f7f9d62709e0c8 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Thu, 9 Oct 2025 03:51:05 +0000 Subject: [PATCH 36/38] [CI Pipeline] Released Snapshot version: 5.58.62-alpha-244-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5e378f102..575f7e187 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.58.61 + 5.58.62-alpha-244-SNAPSHOT UTF-8 From bad6b689b6814741e7404f538e6ae2be05165222 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 9 Oct 2025 13:45:38 +0800 Subject: [PATCH 37/38] Pre-registered raw UID version counters --- .../uid2/operator/model/IdentityVersion.java | 2 + .../operator/service/UIDOperatorService.java | 49 ++++++++++++------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/uid2/operator/model/IdentityVersion.java b/src/main/java/com/uid2/operator/model/IdentityVersion.java index 2c34ac8e6..89b08617d 100644 --- a/src/main/java/com/uid2/operator/model/IdentityVersion.java +++ b/src/main/java/com/uid2/operator/model/IdentityVersion.java @@ -3,6 +3,7 @@ import com.uid2.operator.vertx.ClientInputValidationException; public enum IdentityVersion { + V2(-1), // V2 raw UIDs don't encode version V3(0), V4(1); @@ -18,6 +19,7 @@ public int getValue() { public static IdentityVersion fromValue(int value) { return switch (value) { + case -1 -> V2; case 0 -> V3; case 1 -> V4; default -> throw new ClientInputValidationException("Invalid valid for IdentityVersion: " + value); diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index fad7e2902..3db0c8a3e 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -36,6 +36,8 @@ public class UIDOperatorService implements IUIDOperatorService { private static final Instant REFRESH_CUTOFF = LocalDateTime.parse("2021-03-08T17:00:00", DateTimeFormatter.ISO_LOCAL_DATE_TIME).toInstant(ZoneOffset.UTC); private static final long DAY_IN_MS = Duration.ofDays(1).toMillis(); + private static final Map ADVERTISING_ID_VERSION_COUNTERS = new HashMap<>(); + private final ISaltProvider saltProvider; private final IOptOutStore optOutStore; private final ITokenEncoder encoder; @@ -83,18 +85,10 @@ public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, this.refreshTokenVersion = TokenVersion.V3; this.rawUidV3Enabled = identityV3Enabled; - } - private void validateTokenDurations(Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) { - if (identityExpiresAfter.compareTo(refreshExpiresAfter) > 0) { - throw new IllegalStateException(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " (" + refreshExpiresAfter.toSeconds() + ") < " + IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS + " (" + identityExpiresAfter.toSeconds() + ")"); - } - if (refreshIdentityAfter.compareTo(identityExpiresAfter) > 0) { - throw new IllegalStateException(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS + " (" + identityExpiresAfter.toSeconds() + ") < " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS + " (" + refreshIdentityAfter.toSeconds() + ")"); - } - if (refreshIdentityAfter.compareTo(refreshExpiresAfter) > 0) { - throw new IllegalStateException(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " (" + refreshExpiresAfter.toSeconds() + ") < " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS + " (" + refreshIdentityAfter.toSeconds() + ")"); - } + registerAdvertisingIdVersionCounter(IdentityVersion.V2); + registerAdvertisingIdVersionCounter(IdentityVersion.V3); + registerAdvertisingIdVersionCounter(IdentityVersion.V4); } @Override @@ -208,6 +202,18 @@ public boolean advertisingTokenMatches(String advertisingToken, UserIdentity use return Arrays.equals(mappedIdentity.advertisingId, token.userIdentity.id); } + private void validateTokenDurations(Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter) { + if (identityExpiresAfter.compareTo(refreshExpiresAfter) > 0) { + throw new IllegalStateException(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " (" + refreshExpiresAfter.toSeconds() + ") < " + IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS + " (" + identityExpiresAfter.toSeconds() + ")"); + } + if (refreshIdentityAfter.compareTo(identityExpiresAfter) > 0) { + throw new IllegalStateException(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS + " (" + identityExpiresAfter.toSeconds() + ") < " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS + " (" + refreshIdentityAfter.toSeconds() + ")"); + } + if (refreshIdentityAfter.compareTo(refreshExpiresAfter) > 0) { + throw new IllegalStateException(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS + " (" + refreshExpiresAfter.toSeconds() + ") < " + REFRESH_IDENTITY_TOKEN_AFTER_SECONDS + " (" + refreshIdentityAfter.toSeconds() + ")"); + } + } + private UserIdentity getFirstLevelHashIdentity(UserIdentity userIdentity, Instant asOf) { return getFirstLevelHashIdentity(userIdentity.identityScope, userIdentity.identityType, userIdentity.id, asOf); } @@ -240,15 +246,15 @@ private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt if (salt != null) { if (rawUidV3Enabled) { advertisingId = TokenUtils.getAdvertisingIdV3(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.id, salt); - incrementAdvertisingIdVersionCounter("v3"); + incrementAdvertisingIdVersionCounter(IdentityVersion.V3); } else { advertisingId = TokenUtils.getAdvertisingIdV2(firstLevelHashIdentity.id, salt); - incrementAdvertisingIdVersionCounter("v2"); + incrementAdvertisingIdVersionCounter(IdentityVersion.V2); } } else { try { advertisingId = TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, env, firstLevelHashIdentity.id, key); - incrementAdvertisingIdVersionCounter("v4"); + incrementAdvertisingIdVersionCounter(IdentityVersion.V4); } catch (Exception e) { LOGGER.error("Exception when generating V4 advertising ID", e); advertisingId = null; @@ -258,12 +264,17 @@ private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt return advertisingId; } - private void incrementAdvertisingIdVersionCounter(String version) { - Counter.builder("uid2_raw_uid_version_total") + private void registerAdvertisingIdVersionCounter(IdentityVersion version) { + Counter counter = Counter.builder("uid2_raw_uid_version_total") .description("counter for raw UID version") - .tag("version", version) - .register(Metrics.globalRegistry) - .increment(); + .tag("version", version.toString()) + .register(Metrics.globalRegistry); + + ADVERTISING_ID_VERSION_COUNTERS.put(version, counter); + } + + private void incrementAdvertisingIdVersionCounter(IdentityVersion version) { + ADVERTISING_ID_VERSION_COUNTERS.get(version).increment(); } private byte[] getPreviousAdvertisingId(UserIdentity firstLevelHashIdentity, SaltEntry rotatingSalt, Instant asOf, IdentityEnvironment env) { From a83a0aa11b6549498d2f1fb45d459bc23d7d9a26 Mon Sep 17 00:00:00 2001 From: Gian Miguel Del Mundo Date: Thu, 9 Oct 2025 14:03:36 +0800 Subject: [PATCH 38/38] Changed null advertising ID to throw runtime exception instead --- .../com/uid2/operator/service/TokenUtils.java | 6 +- .../operator/service/UIDOperatorService.java | 9 +- .../uid2/operator/service/V4TokenUtils.java | 28 ++-- .../com/uid2/operator/EncryptionTest.java | 32 +++-- .../operator/UIDOperatorVerticleTest.java | 131 +++++------------- 5 files changed, 76 insertions(+), 130 deletions(-) diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index 2ce744078..89d4c7683 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -58,16 +58,16 @@ public static byte[] getAdvertisingIdV3FromIdentityHash(IdentityScope scope, Ide return getAdvertisingIdV3(scope, type, getFirstLevelHashFromIdentityHash(identityString, firstLevelSalt), rotatingSalt); } - public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte[] firstLevelHash, SaltEntry.KeyMaterial encryptingKey) throws Exception { + public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte[] firstLevelHash, SaltEntry.KeyMaterial encryptingKey) { byte metadata = encodeV4Metadata(scope, type, environment); return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), encryptingKey.salt()); } - public static byte[] getAdvertisingIdV4FromIdentity(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) throws Exception { + public static byte[] getAdvertisingIdV4FromIdentity(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) { return getAdvertisingIdV4(scope, type, environment, getFirstLevelHashFromIdentity(identityString, firstLevelSalt), encryptingKey); } - public static byte[] getAdvertisingIdV4FromIdentityHash(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) throws Exception { + public static byte[] getAdvertisingIdV4FromIdentityHash(IdentityScope scope, IdentityType type, IdentityEnvironment environment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial encryptingKey) { return getAdvertisingIdV4(scope, type, environment, getFirstLevelHashFromIdentityHash(identityString, firstLevelSalt), encryptingKey); } diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index 3db0c8a3e..f053ce3d3 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -252,13 +252,8 @@ private byte[] getAdvertisingId(UserIdentity firstLevelHashIdentity, String salt incrementAdvertisingIdVersionCounter(IdentityVersion.V2); } } else { - try { - advertisingId = TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, env, firstLevelHashIdentity.id, key); - incrementAdvertisingIdVersionCounter(IdentityVersion.V4); - } catch (Exception e) { - LOGGER.error("Exception when generating V4 advertising ID", e); - advertisingId = null; - } + advertisingId = TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, env, firstLevelHashIdentity.id, key); + incrementAdvertisingIdVersionCounter(IdentityVersion.V4); } return advertisingId; diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java index c58091f4c..3e269513a 100644 --- a/src/main/java/com/uid2/operator/service/V4TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -29,21 +29,25 @@ private static byte[] getKeyIdBytes(int keyId) { }; } - public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, int keyId, String key, String salt) throws Exception { - byte[] firstLevelHashLast16Bytes = Arrays.copyOfRange(firstLevelHash, firstLevelHash.length - 16, firstLevelHash.length); - byte[] iv = V4TokenUtils.generateIV(salt, firstLevelHashLast16Bytes, metadata, keyId); - byte[] encryptedFirstLevelHash = V4TokenUtils.encryptHash(key, firstLevelHashLast16Bytes, iv); + public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, int keyId, String key, String salt) { + try { + byte[] firstLevelHashLast16Bytes = Arrays.copyOfRange(firstLevelHash, firstLevelHash.length - 16, firstLevelHash.length); + byte[] iv = V4TokenUtils.generateIV(salt, firstLevelHashLast16Bytes, metadata, keyId); + byte[] encryptedFirstLevelHash = V4TokenUtils.encryptHash(key, firstLevelHashLast16Bytes, iv); - Buffer buffer = Buffer.buffer(); - buffer.appendByte(metadata); - buffer.appendBytes(getKeyIdBytes(keyId)); - buffer.appendBytes(iv); - buffer.appendBytes(encryptedFirstLevelHash); + Buffer buffer = Buffer.buffer(); + buffer.appendByte(metadata); + buffer.appendBytes(getKeyIdBytes(keyId)); + buffer.appendBytes(iv); + buffer.appendBytes(encryptedFirstLevelHash); - byte checksum = generateChecksum(buffer.getBytes()); - buffer.appendByte(checksum); + byte checksum = generateChecksum(buffer.getBytes()); + buffer.appendByte(checksum); - return buffer.getBytes(); + return buffer.getBytes(); + } catch (Exception e) { + throw new RuntimeException(e); + } } public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte metadata, int keyId) throws Exception { diff --git a/src/test/java/com/uid2/operator/EncryptionTest.java b/src/test/java/com/uid2/operator/EncryptionTest.java index 5020b9c92..09f976a50 100644 --- a/src/test/java/com/uid2/operator/EncryptionTest.java +++ b/src/test/java/com/uid2/operator/EncryptionTest.java @@ -5,8 +5,7 @@ import com.uid2.shared.model.EncryptedPayload; import com.uid2.shared.encryption.AesGcm; import com.uid2.shared.model.KeysetKey; -import junit.framework.TestCase; -import org.junit.Assert; +import org.junit.jupiter.api.Test; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -16,12 +15,14 @@ import java.time.Instant; import java.util.concurrent.ThreadLocalRandom; -public class EncryptionTest extends TestCase { +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +class EncryptionTest { private int count = 0; - public void testEncryption() throws Exception { - + @Test + void testEncryption() { final KeysetKey key = new KeysetKey(1, Random.getRandomKeyBytes(), Instant.now(), Instant.now(), Instant.now(), 10); final String testString = "foo@bar.comasdadsjahjhafjhjkfhakjhfkjshdkjfhaskdjfh"; @@ -29,10 +30,11 @@ public void testEncryption() throws Exception { final byte[] decrypted = AesCbc.decrypt(payload.getPayload(), key); final String decryptedString = new String(decrypted, StandardCharsets.UTF_8); - Assert.assertEquals(testString, decryptedString); + assertEquals(testString, decryptedString); } - public void testBenchmark() throws Exception { + @Test + void testBenchmark() { if (System.getenv("SLOW_DEV_URANDOM") != null) { System.err.println("ignore this test since environment variable SLOW_DEV_URANDOM is set"); return; @@ -73,10 +75,14 @@ public void testBenchmark() throws Exception { System.out.println("Decryption Overhead per Entry (ms) = " + overheadPerEntry / (1000000 * 1.0)); // System.out.println("Entries = "+runs+", Base Operation Execution Time (ms) = " + baseTime/(1000000*1.0) + ", With Decryption(ms) = " + decryptTime/(1000000*1.0) + ", Overhead/Entry (ms) = " + ((decryptTime-baseTime)/(runs*1.0)/(1000000*1.0))); + } + private void doSomething(EncryptedPayload ep) { + count++; } - public void testSecureRandom() throws NoSuchAlgorithmException { + @Test + void testSecureRandom() { if (System.getenv("SLOW_DEV_URANDOM") != null) { System.err.println("ignore this test since environment variable SLOW_DEV_URANDOM is set"); return; @@ -109,17 +115,15 @@ public void testSecureRandom() throws NoSuchAlgorithmException { } } - public void testNewInstancesReturned() throws NoSuchAlgorithmException { + @Test + void testNewInstancesReturned() throws NoSuchAlgorithmException { SecureRandom r1 = SecureRandom.getInstance("SHA1PRNG"); SecureRandom r2 = SecureRandom.getInstance("SHA1PRNG"); assertNotSame(r1, r2); } - public void doSomething(EncryptedPayload loag) { - count++; - } - - public void testGCMEncryptionDecryption() { + @Test + void testGCMEncryptionDecryption() { final KeysetKey key = new KeysetKey(1, Random.getRandomKeyBytes(), Instant.now(), Instant.now(), Instant.now(), 10); String plaintxt = "hello world"; EncryptedPayload payload = AesGcm.encrypt(plaintxt.getBytes(StandardCharsets.UTF_8), key); diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index 98ee86643..96fbcc7a5 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -507,14 +507,10 @@ private void checkIdentityMapResponse(JsonObject response, SaltEntry salt, boole String expectedIdentifier = expectedIdentifiers[i]; JsonObject actualMap = mapped.getJsonObject(i); assertEquals(expectedIdentifier, actualMap.getString("identifier")); - try { - if (useHash) { - assertEquals(EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(identityType, expectedIdentifier, firstLevelSalt, salt, useV4Uid, false)), actualMap.getString("advertising_id")); - } else { - assertEquals(EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(identityType, expectedIdentifier, firstLevelSalt, salt, useV4Uid, false)), actualMap.getString("advertising_id")); - } - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); + if (useHash) { + assertEquals(EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(identityType, expectedIdentifier, firstLevelSalt, salt, useV4Uid, false)), actualMap.getString("advertising_id")); + } else { + assertEquals(EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(identityType, expectedIdentifier, firstLevelSalt, salt, useV4Uid, false)), actualMap.getString("advertising_id")); } assertFalse(actualMap.getString("bucket_id").isEmpty()); } @@ -680,7 +676,7 @@ private void assertTokenStatusMetrics(Integer siteId, TokenResponseStatsCollecto assertEquals(1, actual); } - private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean getV4Uid, boolean getPrevUid) throws Exception { + private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean getV4Uid, boolean getPrevUid) { if (getV4Uid) { return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, getPrevUid ? salt.previousKeySalt() : salt.currentKeySalt()); } else { @@ -692,7 +688,7 @@ private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String id return getRawUid(getIdentityScope(), identityType, identityString, firstLevelSalt, rotatingSalt, useRawUidV3()); } - private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) throws Exception { + private byte[] getAdvertisingIdFromIdentity(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) { return getRawUidV4(getIdentityScope(), identityType, IdentityEnvironment.TEST, identityString, firstLevelSalt, rotatingKey); } @@ -708,11 +704,11 @@ public static byte[] getRawUid(IdentityScope identityScope, IdentityType identit : TokenUtils.getAdvertisingIdV3FromIdentity(identityScope, identityType, identityString, firstLevelSalt, rotatingSalt123.currentSalt()); } - public static byte[] getRawUidV4(IdentityScope identityScope, IdentityType identityType, IdentityEnvironment identityEnvironment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) throws Exception { + public static byte[] getRawUidV4(IdentityScope identityScope, IdentityType identityType, IdentityEnvironment identityEnvironment, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) { return TokenUtils.getAdvertisingIdV4FromIdentity(identityScope, identityType, identityEnvironment, identityString, firstLevelSalt, rotatingKey); } - private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean useV4Uid, boolean usePrevUid) throws Exception { + private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry salt, boolean useV4Uid, boolean usePrevUid) { if (useV4Uid) { return getAdvertisingIdFromIdentityHash(identityType, identityString, firstLevelSalt, usePrevUid ? salt.previousKeySalt() : salt.currentKeySalt()); } else { @@ -726,7 +722,7 @@ private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, Strin : TokenUtils.getAdvertisingIdV3FromIdentityHash(getIdentityScope(), identityType, identityString, firstLevelSalt, rotatingSalt); } - private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) throws Exception { + private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) { return TokenUtils.getAdvertisingIdV4FromIdentityHash(getIdentityScope(), identityType, IdentityEnvironment.TEST, identityString, firstLevelSalt, rotatingKey); } @@ -1180,29 +1176,23 @@ void v3IdentityMapMixedInputSuccess( assertEquals(1, mappedPhoneHash.size()); JsonObject mappedPhoneHashExpected; - try { - mappedEmailExpected1 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt, useV4Uid, false)), - "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt, useV4PrevUid, true)), - "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() - ); - - mappedEmailExpected2 = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt, useV4Uid, false)), - "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt, useV4PrevUid, true)), - "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() - ); - - mappedPhoneHashExpected = JsonObject.of( - "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt, useV4Uid, false)), - "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt, useV4PrevUid, true)), - "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() - ); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - testContext.failNow(e); - return; - } + mappedEmailExpected1 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt, useV4Uid, false)), + "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test1@uid2.com", firstLevelSalt, salt, useV4PrevUid, true)), + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() + ); + + mappedEmailExpected2 = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt, useV4Uid, false)), + "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentity(IdentityType.Email, "test2@uid2.com", firstLevelSalt, salt, useV4PrevUid, true)), + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() + ); + + mappedPhoneHashExpected = JsonObject.of( + "u", EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt, useV4Uid, false)), + "p", useV4PrevUid == null ? null : EncodingUtils.toBase64String(getAdvertisingIdFromIdentityHash(IdentityType.Phone, phoneHash, firstLevelSalt, salt, useV4PrevUid, true)), + "r", Instant.ofEpochMilli(salt.refreshFrom()).getEpochSecond() + ); assertEquals(mappedEmailExpected1, mappedEmails.getJsonObject(0)); assertEquals(mappedEmailExpected2, mappedEmails.getJsonObject(1)); assertEquals(mappedPhoneHashExpected, mappedPhoneHash.getJsonObject(0)); @@ -1658,13 +1648,7 @@ void tokenGenerateForEmail(boolean useV4Uid, String contentType, Vertx vertx, Ve assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenGenerated()); assertFalse(PrivacyBits.fromInt(advertisingToken.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - testContext.failNow(e); - return; - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); RefreshToken refreshToken = decodeRefreshToken(encoder, body.getString("decrypted_refresh_token")); assertEquals(clientSiteId, refreshToken.publisherIdentity.siteId); @@ -1754,13 +1738,7 @@ void tokenGenerateThenRefresh( assertNotNull(bodyJson); AdvertisingToken advertisingToken = validateAndGetToken(encoder, bodyJson, IdentityType.Email); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - testContext.failNow(e); - return; - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); String genRefreshToken = bodyJson.getString("refresh_token"); @@ -1777,13 +1755,7 @@ void tokenGenerateThenRefresh( assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenGenerated()); assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenOptedOut()); assertEquals(clientSiteId, adTokenFromRefresh.publisherIdentity.siteId); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - testContext.failNow(e); - return; - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); assertNotEquals(genRefreshToken, refreshTokenStringNew); @@ -1917,13 +1889,7 @@ void tokenGenerateThenValidateWithEmail_Match(boolean useV4Uid, Vertx vertx, Ver String advertisingTokenString = genBody.getString("advertising_token"); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, IdentityType.Email); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - testContext.failNow(e); - return; - } + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); JsonObject v2Payload = new JsonObject(); v2Payload.put("token", advertisingTokenString); @@ -4192,9 +4158,9 @@ void cstgUserOptsOutAfterTokenGenerate( } // tests for opted out user should lead to generating ad tokens with optout success response - // tests for non-opted out user should generate the UID2 identity and the generated refresh token can be - // refreshed again - // tests for all email/phone combos +// tests for non-opted out user should generate the UID2 identity and the generated refresh token can be +// refreshed again +// tests for all email/phone combos @ParameterizedTest @CsvSource({ // After - v4 UID, refreshed v4 UID @@ -4259,13 +4225,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( decodeV2RefreshToken(respJson); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - testContext.failNow(e); - return; - } + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); @@ -4290,13 +4250,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( //make sure the new advertising token from refresh looks right AdvertisingToken adTokenFromRefresh = validateAndGetToken(encoder, refreshBody, identityType); - try { - assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - testContext.failNow(e); - return; - } + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); assertNotEquals(genRefreshToken, refreshTokenStringNew); @@ -4413,20 +4367,9 @@ private void assertAreClientSideGeneratedTokens(AdvertisingToken advertisingToke final byte[] advertisingId; if (key == null) { - advertisingId = getAdvertisingIdFromIdentity(identityType, - identityString, - firstLevelSalt, - rotatingSalt123.currentSalt()); + advertisingId = getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, rotatingSalt123.currentSalt()); } else { - try { - advertisingId = getAdvertisingIdFromIdentity(identityType, - identityString, - firstLevelSalt, - key); - } catch (Exception e) { - org.junit.jupiter.api.Assertions.fail(e.getMessage()); - return; - } + advertisingId = getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, key); } final byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity(identityString, firstLevelSalt);