diff --git a/src/main/java/com/uid2/operator/model/IdentityEnvironment.java b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java index 9ca75b03c..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; @@ -34,4 +36,4 @@ public static IdentityEnvironment fromString(String value) { default -> throw new ClientInputValidationException("Invalid valid for IdentityEnvironment: " + value); }; } -} \ No newline at end of file +} 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/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 new file mode 100644 index 000000000..89b08617d --- /dev/null +++ b/src/main/java/com/uid2/operator/model/IdentityVersion.java @@ -0,0 +1,28 @@ +package com.uid2.operator.model; + +import com.uid2.operator.vertx.ClientInputValidationException; + +public enum IdentityVersion { + V2(-1), // V2 raw UIDs don't encode version + V3(0), + V4(1); + + private final int value; + + IdentityVersion(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + 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/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/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; diff --git a/src/main/java/com/uid2/operator/model/UserIdentity.java b/src/main/java/com/uid2/operator/model/UserIdentity.java index 760d0ffb6..17c0f74b8 100644 --- a/src/main/java/com/uid2/operator/model/UserIdentity.java +++ b/src/main/java/com/uid2/operator/model/UserIdentity.java @@ -2,7 +2,6 @@ import java.time.Instant; import java.util.Arrays; -import java.util.Objects; public class UserIdentity { public final IdentityScope identityScope; 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/EncryptedTokenEncoder.java b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java index f151f0919..402b672b3 100644 --- a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java +++ b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java @@ -231,7 +231,6 @@ public AdvertisingToken decodeAdvertisingTokenV2(Buffer b) { } catch (Exception e) { throw new RuntimeException("Couldn't decode advertisingTokenV2", e); } - } public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, TokenVersion tokenVersion) { @@ -253,8 +252,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"); } @@ -338,7 +336,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 +364,16 @@ private byte[] encryptIdentityV2(PublisherIdentity publisherIdentity, UserIdenti } } - static private byte encodeIdentityTypeV3(UserIdentity userIdentity) { - return (byte) (TokenUtils.encodeIdentityScope(userIdentity.identityScope) | (userIdentity.identityType.value << 2) | 3); + private static byte encodeIdentityTypeV3(UserIdentity userIdentity) { + 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 } - 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 +389,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 c60839200..ea8d82b1d 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, IdentityEnvironment env); @@ -25,6 +24,4 @@ public interface IUIDOperatorService { void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, IdentityEnvironment env, Handler> handler); boolean advertisingTokenMatches(String advertisingToken, UserIdentity userIdentity, Instant asOf, IdentityEnvironment env); - - 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..33a862c38 100644 --- a/src/main/java/com/uid2/operator/service/InputUtil.java +++ b/src/main/java/com/uid2/operator/service/InputUtil.java @@ -6,12 +6,13 @@ 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(); @@ -49,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'; } @@ -65,8 +65,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; @@ -174,12 +173,12 @@ public enum IdentityInputType { Hash } - private static enum EmailParsingState { + private enum EmailParsingState { Starting, Pre, SubDomain, Domain, - Terminal, + Terminal } public static class InputVal { @@ -255,7 +254,9 @@ public IdentityType getIdentityType() { return identityType; } - public IdentityInputType getInputType() { return inputType; } + public IdentityInputType getInputType() { + return inputType; + } public boolean isValid() { return valid; @@ -271,5 +272,4 @@ public UserIdentity toUserIdentity(IdentityScope identityScope, int 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 2cabc641b..89d4c7683 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -1,12 +1,15 @@ 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.IdentityVersion; +import com.uid2.shared.model.SaltEntry; -import java.util.HashSet; -import java.util.Set; +public final class TokenUtils { + private TokenUtils() { + } -public class 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,11 +58,36 @@ 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) { + 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) { + 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) { + 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.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.getValue() << 5); + } + + public static byte encodeIdentityEnvironment(IdentityEnvironment identityEnvironment) { + return (byte) (identityEnvironment.getValue() << 6); } } diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index 45f3a5b07..f053ce3d3 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -7,9 +7,13 @@ 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.time.Clock; import java.time.Duration; @@ -23,6 +27,8 @@ import static java.time.temporal.ChronoUnit.DAYS; 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"; @@ -30,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; @@ -77,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 @@ -110,7 +110,7 @@ public IdentityTokens generateIdentity(IdentityRequest request, Duration refresh @Override public RefreshResponse refreshIdentity(RefreshToken token, Duration refreshIdentityAfter, Duration refreshExpiresAfter, Duration identityExpiresAfter, IdentityEnvironment env) { this.validateTokenDurations(refreshIdentityAfter, refreshExpiresAfter, identityExpiresAfter); - // should not be possible as different scopes should be using different keys, but just in case + // should not be possible as different scopes/environments should be using different keys, but just in case if (token.userIdentity.identityScope != this.identityScope) { return RefreshResponse.Invalid; } @@ -180,7 +180,7 @@ private ISaltProvider.ISaltSnapshot getSaltProviderSnapshot(Instant asOf) { } @Override - public void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, IdentityEnvironment env, Handler> handler) { + public void invalidateTokensAsync(UserIdentity userIdentity, Instant asOf, String uidTraceId, IdentityEnvironment env, Handler> handler) { final UserIdentity firstLevelHashIdentity = getFirstLevelHashIdentity(userIdentity, asOf); final MappedIdentity mappedIdentity = getMappedIdentity(firstLevelHashIdentity, asOf, env); @@ -202,10 +202,16 @@ 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 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) { @@ -223,8 +229,8 @@ private byte[] getFirstLevelHash(byte[] identityHash, Instant asOf) { private MappedIdentity getMappedIdentity(UserIdentity firstLevelHashIdentity, Instant asOf, IdentityEnvironment env) { final SaltEntry rotatingSalt = getSaltProviderSnapshot(asOf).getRotatingSalt(firstLevelHashIdentity.id); - final byte[] advertisingId = getAdvertisingId(firstLevelHashIdentity, rotatingSalt.currentSalt()); - final byte[] previousAdvertisingId = getPreviousAdvertisingId(firstLevelHashIdentity, rotatingSalt, asOf); + final byte[] advertisingId = getAdvertisingId(firstLevelHashIdentity, rotatingSalt.currentSalt(), rotatingSalt.currentKeySalt(), env); + final byte[] previousAdvertisingId = getPreviousAdvertisingId(firstLevelHashIdentity, rotatingSalt, asOf, env); final long refreshFrom = getRefreshFrom(rotatingSalt, asOf); return new MappedIdentity( @@ -234,19 +240,49 @@ private MappedIdentity getMappedIdentity(UserIdentity firstLevelHashIdentity, In 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, IdentityEnvironment env) { + byte[] advertisingId; + + if (salt != null) { + if (rawUidV3Enabled) { + advertisingId = TokenUtils.getAdvertisingIdV3(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, firstLevelHashIdentity.id, salt); + incrementAdvertisingIdVersionCounter(IdentityVersion.V3); + } else { + advertisingId = TokenUtils.getAdvertisingIdV2(firstLevelHashIdentity.id, salt); + incrementAdvertisingIdVersionCounter(IdentityVersion.V2); + } + } else { + advertisingId = TokenUtils.getAdvertisingIdV4(firstLevelHashIdentity.identityScope, firstLevelHashIdentity.identityType, env, firstLevelHashIdentity.id, key); + incrementAdvertisingIdVersionCounter(IdentityVersion.V4); + } + + return advertisingId; + } + + private void registerAdvertisingIdVersionCounter(IdentityVersion version) { + Counter counter = Counter.builder("uid2_raw_uid_version_total") + .description("counter for raw UID version") + .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) { + private byte[] getPreviousAdvertisingId(UserIdentity firstLevelHashIdentity, SaltEntry rotatingSalt, Instant asOf, IdentityEnvironment env) { long age = asOf.toEpochMilli() - rotatingSalt.lastUpdated(); if (age / DAY_IN_MS < 90) { - if (rotatingSalt.previousSalt() == null || rotatingSalt.previousSalt().isBlank()) { + boolean missingSalt = rotatingSalt.previousSalt() == null; + boolean missingKey = rotatingSalt.previousKeySalt() == null + || rotatingSalt.previousKeySalt().key() == null || rotatingSalt.previousKeySalt().salt() == null; + + if (missingSalt && missingKey) { return null; } - return getAdvertisingId(firstLevelHashIdentity, rotatingSalt.previousSalt()); + return getAdvertisingId(firstLevelHashIdentity, rotatingSalt.previousSalt(), rotatingSalt.previousKeySalt(), env); } return null; } @@ -288,12 +324,12 @@ private AdvertisingToken createAdvertisingToken(PublisherIdentity publisherIdent 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; time = providedTime; 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..3e269513a --- /dev/null +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -0,0 +1,88 @@ +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.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() { + } + + 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) { + 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); + + byte checksum = generateChecksum(buffer.getBytes()); + buffer.appendByte(checksum); + + return buffer.getBytes(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + 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(getKeyIdBytes(keyId)); + return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(ivBase.toByteArray()), 0, IV_LENGTH); + } + + public static byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception { + // Set up AES256-CTR cipher + Cipher aesCtr = CIPHER.get(); + 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; + } + return checksum; + } + + 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/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index b531e13ea..439cb3c46 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -78,17 +78,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 +108,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<>(); @@ -126,8 +128,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<>(); @@ -137,19 +139,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, @@ -270,7 +269,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( @@ -300,7 +298,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) { @@ -376,7 +373,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; @@ -452,7 +449,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; @@ -462,15 +458,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); } @@ -490,11 +483,11 @@ else if(emailHash != null) { input.toUserIdentity(this.identityScope, privacyBits.getAsInt(), Instant.now()), OptoutCheckPolicy.RespectOptOut, identityEnvironment - ), + ), 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; } @@ -504,11 +497,10 @@ 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 + // 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); } @@ -1470,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) { 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,,, 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/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/TokenEncodingTest.java b/src/test/java/com/uid2/operator/TokenEncodingTest.java index 15030df98..e7816776d 100644 --- a/src/test/java/com/uid2/operator/TokenEncodingTest.java +++ b/src/test/java/com/uid2/operator/TokenEncodingTest.java @@ -23,14 +23,13 @@ import static org.junit.jupiter.api.Assertions.*; -public class TokenEncodingTest { - +class TokenEncodingTest { private final KeyManager keyManager; public TokenEncodingTest() throws Exception { RotatingKeysetKeyStore keysetKeyStore = new RotatingKeysetKeyStore( - new EmbeddedResourceStorage(Main.class), - new GlobalScope(new CloudPath("/com.uid2.core/test/keyset_keys/metadata.json"))); + new EmbeddedResourceStorage(Main.class), + new GlobalScope(new CloudPath("/com.uid2.core/test/keyset_keys/metadata.json"))); JsonObject m1 = keysetKeyStore.getMetadata(); keysetKeyStore.loadContent(m1); @@ -47,18 +46,18 @@ public TokenEncodingTest() throws Exception { @ParameterizedTest @EnumSource(value = TokenVersion.class, names = {"V3", "V4"}) - public void testRefreshTokenEncoding(TokenVersion tokenVersion) { + void testRefreshTokenEncoding(TokenVersion tokenVersion) { final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager); final Instant now = EncodingUtils.NowUTCMillis(); final byte[] firstLevelHash = TokenUtils.getFirstLevelHashFromIdentity("test@example.com", "some-salt"); final RefreshToken token = new RefreshToken(tokenVersion, - now, - now.plusSeconds(360), - new OperatorIdentity(101, OperatorType.Service, 102, 103), - new PublisherIdentity(111, 112, 113), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, firstLevelHash, 121, now, now.minusSeconds(122)) + now, + now.plusSeconds(360), + new OperatorIdentity(101, OperatorType.Service, 102, 103), + new PublisherIdentity(111, 112, 113), + new UserIdentity(IdentityScope.UID2, IdentityType.Email, firstLevelHash, 121, now, now.minusSeconds(122)) ); if (tokenVersion == TokenVersion.V4) { @@ -87,28 +86,28 @@ public void testRefreshTokenEncoding(TokenVersion tokenVersion) { } @ParameterizedTest - @CsvSource({"false, V4", //same as current UID2 prod (as at 2024-12-10) + @CsvSource({ + "false, V4", //same as current UID2 prod (as at 2024-12-10) "true, V4", //same as current EUID prod (as at 2024-12-10) //the following combinations aren't used in any UID2/EUID environments but just testing them regardless "false, V3", "true, V3", "false, V2", - "true, V2", - } - ) - public void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVersion) { + "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, - now, - now.plusSeconds(60), - new OperatorIdentity(101, OperatorType.Service, 102, 103), - new PublisherIdentity(111, 112, 113), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, rawUid, 121, now, now.minusSeconds(122)) + adTokenVersion, + now, + now.plusSeconds(60), + new OperatorIdentity(101, OperatorType.Service, 102, 103), + new PublisherIdentity(111, 112, 113), + 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 b0b2825c4..1c35ac1e3 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorServiceTest.java @@ -42,26 +42,26 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -public class UIDOperatorServiceTest { +class UIDOperatorServiceTest { + private static final String FIRST_LEVEL_SALT = "first-level-salt"; + private static final int IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS = 600; + private static final int REFRESH_TOKEN_EXPIRES_AFTER_SECONDS = 900; + private static final int REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = 300; + private AutoCloseable mocks; @Mock private IOptOutStore optOutStore; @Mock private Clock clock; @Mock private OperatorShutdownHandler shutdownHandler; - EncryptedTokenEncoder tokenEncoder; - UidInstanceIdProvider uidInstanceIdProvider; - JsonObject uid2Config; - JsonObject euidConfig; - ExtendedUIDOperatorService uid2Service; - ExtendedUIDOperatorService euidService; - Instant now; - - final int IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS = 600; - final int REFRESH_TOKEN_EXPIRES_AFTER_SECONDS = 900; - final int REFRESH_IDENTITY_TOKEN_AFTER_SECONDS = 300; - final String FIRST_LEVEL_SALT = "first-level-salt"; + private EncryptedTokenEncoder tokenEncoder; + private UidInstanceIdProvider uidInstanceIdProvider; + private JsonObject uid2Config; + private JsonObject euidConfig; + private ExtendedUIDOperatorService uid2Service; + private ExtendedUIDOperatorService euidService; + private Instant now; - class ExtendedUIDOperatorService extends UIDOperatorService { + static class ExtendedUIDOperatorService extends UIDOperatorService { public ExtendedUIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, ITokenEncoder encoder, Clock clock, IdentityScope identityScope, Handler saltRetrievalResponseHandler, boolean identityV3Enabled, UidInstanceIdProvider uidInstanceIdProvider) { super(optOutStore, saltProvider, encoder, clock, identityScope, saltRetrievalResponseHandler, identityV3Enabled, uidInstanceIdProvider); } @@ -183,8 +183,8 @@ 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), @@ -201,7 +201,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); @@ -242,7 +243,7 @@ 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( @@ -271,7 +272,7 @@ 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( @@ -291,7 +292,7 @@ public void testTestOptOutKey_RespectOptout() { } @Test - public void testTestOptOutKeyIdentityScopeMismatch() { + void testTestOptOutKeyIdentityScopeMismatch() { final String email = "optout@example.com"; final InputUtil.InputVal inputVal = InputUtil.normalizeEmail(email); @@ -326,7 +327,7 @@ public void testTestOptOutKeyIdentityScopeMismatch() { "Email,test@example.com,EUID", "Phone,+01010101010,UID2", "Phone,+01010101010,EUID"}) - public void testGenerateTokenForOptOutUser(IdentityType type, String identity, IdentityScope scope) { + void testGenerateTokenForOptOutUser(IdentityType type, String identity, IdentityScope scope) { final UserIdentity userIdentity = createUserIdentity(identity, scope, type); final IdentityRequest identityRequestForceGenerate = new IdentityRequest( @@ -394,7 +395,7 @@ public void testGenerateTokenForOptOutUser(IdentityType type, String identity, I "Email,test@example.com,EUID", "Phone,+01010101010,UID2", "Phone,+01010101010,EUID"}) - public void testIdentityMapForOptOutUser(IdentityType type, String identity, IdentityScope scope) { + void testIdentityMapForOptOutUser(IdentityType type, String identity, IdentityScope scope) { final UserIdentity userIdentity = createUserIdentity(identity, scope, type); final Instant now = Instant.now(); @@ -445,28 +446,20 @@ private enum TestIdentityInputType { public final int type; - TestIdentityInputType(int type) { this.type = type; } + TestIdentityInputType(int type) { + this.type = type; + } } private InputUtil.InputVal generateInputVal(TestIdentityInputType type, String id) { - InputUtil.InputVal inputVal; - switch(type) { - case Email: - inputVal = InputUtil.normalizeEmail(id); - break; - case Phone: - inputVal = InputUtil.normalizePhone(id); - break; - case EmailHash: - inputVal = InputUtil.normalizeEmailHash(EncodingUtils.getSha256(id)); - break; - default: //PhoneHash - inputVal = InputUtil.normalizePhoneHash(EncodingUtils.getSha256(id)); - } - return inputVal; + return switch (type) { + case Email -> InputUtil.normalizeEmail(id); + case Phone -> InputUtil.normalizePhone(id); + case EmailHash -> InputUtil.normalizeEmailHash(EncodingUtils.getSha256(id)); + default -> InputUtil.normalizePhoneHash(EncodingUtils.getSha256(id)); + }; } - //UID2-1224 @ParameterizedTest @CsvSource({"Email,optout@example.com,UID2", @@ -491,14 +484,13 @@ void testSpecialIdentityOptOutTokenGenerate(TestIdentityInputType type, String i when(this.optOutStore.getLatestEntry(any())).thenReturn(null); IdentityTokens tokens; - 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)); - } - else { + } else { tokens = uid2Service.generateIdentity( identityRequest, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), @@ -532,10 +524,9 @@ void testSpecialIdentityOptOutIdentityMap(TestIdentityInputType type, String id, when(this.optOutStore.getLatestEntry(any())).thenReturn(null); final MappedIdentity mappedIdentity; - if(scope == IdentityScope.EUID) { + if (scope == IdentityScope.EUID) { mappedIdentity = euidService.mapIdentity(mapRequestRespectOptOut); - } - else { + } else { mappedIdentity = uid2Service.mapIdentity(mapRequestRespectOptOut); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); @@ -564,14 +555,13 @@ void testSpecialIdentityOptOutTokenRefresh(TestIdentityInputType type, String id ); IdentityTokens tokens; - 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)); - } - else { + } else { tokens = uid2Service.generateIdentity( identityRequest, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), @@ -620,14 +610,13 @@ void testSpecialIdentityRefreshOptOutGenerate(TestIdentityInputType type, String when(this.optOutStore.getLatestEntry(any())).thenReturn(Instant.now()); IdentityTokens tokens; - 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)); - } - else { + } else { tokens = uid2Service.generateIdentity( identityRequest, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), @@ -675,10 +664,9 @@ void testSpecialIdentityRefreshOptOutIdentityMap(TestIdentityInputType type, Str when(this.optOutStore.getLatestEntry(any())).thenReturn(Instant.now()); final MappedIdentity mappedIdentity; - if(scope == IdentityScope.EUID) { + if (scope == IdentityScope.EUID) { mappedIdentity = euidService.mapIdentity(mapRequestRespectOptOut); - } - else { + } else { mappedIdentity = uid2Service.mapIdentity(mapRequestRespectOptOut); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); @@ -717,8 +705,7 @@ void testSpecialIdentityValidateGenerate(TestIdentityInputType type, String id, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), Duration.ofSeconds(REFRESH_TOKEN_EXPIRES_AFTER_SECONDS), Duration.ofSeconds(IDENTITY_TOKEN_EXPIRES_AFTER_SECONDS)); - } - else { + } else { tokens = uid2Service.generateIdentity( identityRequest, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), @@ -733,7 +720,6 @@ void testSpecialIdentityValidateGenerate(TestIdentityInputType type, String id, assertNotNull(advertisingToken.userIdentity); } - @ParameterizedTest @CsvSource({"Email,validate@example.com,UID2", "EmailHash,validate@example.com,UID2", @@ -756,10 +742,9 @@ void testSpecialIdentityValidateIdentityMap(TestIdentityInputType type, String i when(this.optOutStore.getLatestEntry(any())).thenReturn(Instant.now()); final MappedIdentity mappedIdentity; - if(scope == IdentityScope.EUID) { + if (scope == IdentityScope.EUID) { mappedIdentity = euidService.mapIdentity(mapRequestRespectOptOut); - } - else { + } else { mappedIdentity = uid2Service.mapIdentity(mapRequestRespectOptOut); } verify(shutdownHandler, atLeastOnce()).handleSaltRetrievalResponse(false); @@ -784,14 +769,13 @@ void testNormalIdentityOptIn(TestIdentityInputType type, String id, IdentityScop IdentityEnvironment.TEST ); IdentityTokens tokens; - 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)); - } - else { + } else { tokens = uid2Service.generateIdentity( identityRequest, Duration.ofSeconds(REFRESH_IDENTITY_TOKEN_AFTER_SECONDS), @@ -863,15 +847,14 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String 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), @@ -906,10 +889,9 @@ void testExpiredSaltsNotifiesShutdownHandler(TestIdentityInputType type, String IdentityEnvironment.TEST); 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); @@ -935,8 +917,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); } @@ -957,7 +939,7 @@ void testMappedIdentityWithOutdatedPreviousSaltReturnsNoPreviousUid(long extraMs MapRequest mapRequest = new MapRequest(emailInput.toUserIdentity(IdentityScope.UID2, 0, this.now), OptoutCheckPolicy.RespectOptOut, now, IdentityEnvironment.TEST); 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); } @@ -978,7 +960,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 c79463adc..96fbcc7a5 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -59,17 +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.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; @@ -79,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.*; @@ -86,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"; @@ -106,6 +105,8 @@ public class UIDOperatorVerticleTest { 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 @@ -136,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) { @@ -164,11 +166,12 @@ void deployVerticle(Vertx vertx, VertxTestContext testContext, TestInfo testInfo 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(); Metrics.globalRegistry.add(registry); + + this.encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); } @AfterEach @@ -358,7 +361,6 @@ private void sendTokenRefresh(Vertx vertx, VertxTestContext testContext, String handler.handle(tryParseResponse(response)); } }))); - } private String decodeV2RefreshToken(JsonObject respJson) { @@ -388,10 +390,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); } @@ -486,7 +484,7 @@ private enum KeyDownloadEndpoint { SHARING("/key/sharing"), BIDSTREAM("/key/bidstream"); - private String path; + private final String path; KeyDownloadEndpoint(String path) { this.path = path; @@ -497,24 +495,78 @@ public String getPath() { } } - private void checkIdentityMapResponse(JsonObject response, 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"); 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()); + 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()); } } - protected void setupSalts() { + private void checkIdentityMapResponse(JsonObject response) { + checkIdentityMapResponse(response, null, false, null, false); + } + + protected SaltEntry setupSalts(boolean useV4Uid, Boolean useV4PrevUid) { + return useV4Uid ? setupSaltsForV4Uid(useV4PrevUid) : setupSaltsForV2V3Uid(useV4PrevUid); + } + + protected SaltEntry setupSalts(boolean useV4Uid) { + return setupSalts(useV4Uid, null); + } + + protected SaltEntry setupSalts() { + return setupSalts(false, null); + } + + protected SaltEntry setupSaltsForV2V3Uid(Boolean useV4PrevUid) { when(saltProviderSnapshot.getFirstLevelSalt()).thenReturn(firstLevelSalt); - when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(rotatingSalt123); + + var lastUpdated = Instant.now().minus(1, DAYS); + var refreshFrom = lastUpdated.plus(30, DAYS); + SaltEntry salt = new SaltEntry( + rotatingSalt123.id(), + rotatingSalt123.hashedId(), + lastUpdated.toEpochMilli(), + rotatingSalt123.currentSalt(), + refreshFrom.toEpochMilli(), + 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 setupSaltsForV4Uid(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", + lastUpdated.toEpochMilli(), + null, + refreshFrom.toEpochMilli(), + useV4PrevUid == null || useV4PrevUid ? null : "salt123", + new SaltEntry.KeyMaterial(1000000, "key12345key12345key12345key12345", "salt1234salt1234salt1234salt1234"), + useV4PrevUid == null || !useV4PrevUid ? null : new SaltEntry.KeyMaterial(1000001, "key12345key12345key12345key12346", "salt1234salt1234salt1234salt1235")); + when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); + return salt; } private HashMap keysetsToMap(Keyset... keysets) { @@ -624,28 +676,56 @@ private void assertTokenStatusMetrics(Integer siteId, TokenResponseStatsCollecto assertEquals(1, actual); } + 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 { + return getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, getPrevUid ? salt.previousSalt() : salt.currentSalt()); + } + } + 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) { + 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) { + return TokenUtils.getAdvertisingIdV4FromIdentity(identityScope, identityType, identityEnvironment, identityString, firstLevelSalt, rotatingKey); + } + + 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 { + 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) : TokenUtils.getAdvertisingIdV3FromIdentityHash(getIdentityScope(), identityType, identityString, firstLevelSalt, rotatingSalt); } + private byte[] getAdvertisingIdFromIdentityHash(IdentityType identityType, String identityString, String firstLevelSalt, SaltEntry.KeyMaterial rotatingKey) { + return TokenUtils.getAdvertisingIdV4FromIdentityHash(getIdentityScope(), identityType, IdentityEnvironment.TEST, identityString, firstLevelSalt, rotatingKey); + } + private JsonObject createBatchEmailsRequestPayload() { JsonArray emails = new JsonArray(); emails.add("test1@uid2.com"); @@ -687,10 +767,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 = { @@ -763,6 +840,7 @@ void keyLatestHideRefreshKey(Vertx vertx, VertxTestContext testContext) { }; 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 -> { System.out.println(respJson); checkEncryptionKeysResponse(respJson, @@ -867,10 +945,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); @@ -931,7 +1006,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); @@ -1044,19 +1119,38 @@ void identityMapNewClientWrongPolicySpecifiedOlderKeySuccessful(String policyPar } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" + @CsvSource({ + // 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(String contentType, Vertx vertx, VertxTestContext testContext) { + 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 = setupSalts(useV4Uid, useV4PrevUid); when(saltProviderSnapshot.getRotatingSalt(any())).thenReturn(salt); var phoneHash = TokenUtils.getIdentityHashString("+15555555555"); @@ -1075,54 +1169,52 @@ void v3IdentityMapMixedInputSuccess(String contentType, Vertx vertx, VertxTestCo 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() + var mappedPhoneHash = body.getJsonArray("phone_hash"); + assertEquals(1, mappedPhoneHash.size()); + JsonObject mappedPhoneHashExpected; + + 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() ); - 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() + 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)); 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)); } - @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(); + setupSalts(useV4Uid); // 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"] } """ @@ -1441,8 +1533,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, @@ -1529,14 +1619,21 @@ void tokenGenerateOptOutTokenWithDisableOptoutTokenFF(String policyParameterKey, } @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void tokenGenerateForEmail(String contentType, Vertx vertx, VertxTestContext testContext) { + @CsvSource({ + "true,text/plain", + "true,application/octet-stream", + + "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); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(useV4Uid); + JsonObject v2Payload = new JsonObject(); v2Payload.put("email", emailAddress); @@ -1545,14 +1642,13 @@ 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)); 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); + 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); @@ -1585,7 +1681,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); @@ -1607,17 +1702,33 @@ void tokenGenerateForEmailHash(Vertx vertx, VertxTestContext testContext) { } @ParameterizedTest - @ValueSource(strings = { - "text/plain", - "application/octet-stream" + @CsvSource({ + // 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(String contentType, Vertx vertx, VertxTestContext testContext) { + 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); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(useV4Uid); + Map additionalHeaders = Map.of(ClientVersionHeader, iosClientVersionHeaderValue, HttpHeaders.CONTENT_TYPE.toString(), contentType); @@ -1626,22 +1737,25 @@ void tokenGenerateThenRefresh(String contentType, Vertx vertx, VertxTestContext JsonObject bodyJson = genRespJson.getJsonObject("body"); assertNotNull(bodyJson); + AdvertisingToken advertisingToken = validateAndGetToken(encoder, bodyJson, IdentityType.Email); + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); + String genRefreshToken = bodyJson.getString("refresh_token"); when(this.optOutStore.getLatestEntry(any())).thenReturn(null); + 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"); assertNotNull(refreshBody); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); - AdvertisingToken advertisingToken = validateAndGetToken(encoder, refreshBody, IdentityType.Email); + AdvertisingToken adTokenFromRefresh = 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); + assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenGenerated()); + assertFalse(PrivacyBits.fromInt(adTokenFromRefresh.userIdentity.privacyBits).isClientSideTokenOptedOut()); + assertEquals(clientSiteId, adTokenFromRefresh.publisherIdentity.siteId); + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); assertNotEquals(genRefreshToken, refreshTokenStringNew); @@ -1693,7 +1807,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); @@ -1759,20 +1872,24 @@ void tokenGenerateThenRefreshNoActiveKey(Vertx vertx, VertxTestContext testConte }); } - @Test - void tokenGenerateThenValidateWithEmail_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); - setupSalts(); setupKeys(); + SaltEntry salt = setupSalts(useV4Uid); + generateTokens(vertx, "email", emailAddress, genRespJson -> { assertEquals("success", genRespJson.getString("status")); JsonObject genBody = genRespJson.getJsonObject("body"); assertNotNull(genBody); String advertisingTokenString = genBody.getString("advertising_token"); + AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, IdentityType.Email); + assertArrayEquals(getAdvertisingIdFromIdentity(IdentityType.Email, emailAddress, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); JsonObject v2Payload = new JsonObject(); v2Payload.put("token", advertisingTokenString); @@ -1861,7 +1978,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); @@ -1892,7 +2008,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); @@ -1977,13 +2092,18 @@ 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(); + setupSalts(useV4Uid); + 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; @@ -2075,10 +2195,13 @@ 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"; + setupSalts(useV4Uid); + generateRefreshToken(vertx, "email", emailAddress, clientSiteId, genRespJson -> { JsonObject bodyJson = genRespJson.getJsonObject("body"); String refreshToken = bodyJson.getString("refresh_token"); @@ -2097,17 +2220,25 @@ void tokenRefreshOptOut(Vertx vertx, VertxTestContext testContext) { }); } - @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)); + setupSalts(useRefreshedV4Uid); + sendTokenRefresh(vertx, testContext, refreshToken, refreshTokenDecryptSecret, 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); assertNull(refreshRespJson.getJsonObject("body")); @@ -2118,10 +2249,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; @@ -2158,7 +2286,6 @@ void tokenValidateWithEmailHash_Mismatch(Vertx vertx, VertxTestContext testConte }); } - @Test void identityMapBatchBothEmailAndHashEmpty(Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; @@ -2172,8 +2299,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(); }); } @@ -2226,7 +2353,6 @@ void identityMapSingleEmailProvided(Vertx vertx, VertxTestContext testContext) { setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("email", "test@example.com"); send(vertx, "v2/identity/map", req, 400, json -> { @@ -2246,7 +2372,6 @@ void identityMapSingleEmailHashProvided(Vertx vertx, VertxTestContext testContex setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("email_hash", "test@example.com"); send(vertx, "v2/identity/map", req, 400, json -> { @@ -2266,7 +2391,6 @@ void identityMapSinglePhoneProvided(Vertx vertx, VertxTestContext testContext) { setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("phone", "555-555-5555"); send(vertx, "v2/identity/map", req, 400, json -> { @@ -2286,7 +2410,6 @@ void identityMapSinglePhoneHashProvided(Vertx vertx, VertxTestContext testContex setupKeys(); JsonObject req = new JsonObject(); - JsonArray emailHashes = new JsonArray(); req.put("phone_hash", "555-555-5555"); send(vertx, "v2/identity/map", req, 400, json -> { @@ -2298,17 +2421,19 @@ 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(); + SaltEntry salt = setupSalts(useV4Uid); + JsonObject req = createBatchEmailsRequestPayload(); send(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "test1@uid2.com", "test2@uid2.com"); + checkIdentityMapResponse(json, salt, useV4Uid, IdentityType.Email, false, "test1@uid2.com", "test2@uid2.com"); testContext.completeNow(); }); } @@ -2317,23 +2442,24 @@ void identityMapBatchEmails(Vertx vertx, VertxTestContext testContext) { 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); - 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(vertx, "v2/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, email_hashes); + checkIdentityMapResponse(json, salt, false, IdentityType.Email, true, emailHashes); testContext.completeNow(); }); } @@ -2342,9 +2468,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); @@ -2354,7 +2481,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(); }); } @@ -2411,19 +2538,24 @@ 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(); + setupSalts(useV4Uid); JsonArray rawUIDs = new JsonArray(); for (String rawUID2 : optedOutIds.keySet()) { @@ -2496,12 +2628,18 @@ void optOutStatusUnauthorized(String contentType, Vertx vertx, VertxTestContext } @ParameterizedTest - @ValueSource(strings = {"text/plain", "application/octet-stream"}) - void LogoutV2(String contentType, Vertx vertx, VertxTestContext testContext) { + @CsvSource({ + "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(); + setupSalts(useV4Uid); JsonObject req = new JsonObject(); req.put("email", "test@uid2.com"); @@ -2521,7 +2659,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); @@ -2627,7 +2765,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); @@ -2664,7 +2801,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); @@ -2706,7 +2842,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); @@ -2820,7 +2955,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); @@ -2839,7 +2973,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")); @@ -2848,11 +2982,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(); @@ -2863,8 +2995,8 @@ void identityMapBatchBothPhoneAndHashEmpty(Vertx vertx, VertxTestContext testCon req.put("phone", phones); req.put("phone_hash", phoneHashes); - send(vertx, apiVersion + "/identity/map", req, 200, respJson -> { - checkIdentityMapResponse(respJson); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json); testContext.completeNow(); }); } @@ -2872,7 +3004,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(); @@ -2886,7 +3017,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(); @@ -2896,11 +3027,11 @@ 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(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray phones = new JsonArray(); req.put("phone", phones); @@ -2908,8 +3039,8 @@ void identityMapBatchPhones(Vertx vertx, VertxTestContext testContext) { phones.add("+15555555555"); phones.add("+15555555556"); - send(vertx, apiVersion + "/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "+15555555555", "+15555555556"); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json, salt, false, IdentityType.Phone, false, "+15555555555", "+15555555556"); testContext.completeNow(); }); } @@ -2917,25 +3048,25 @@ 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(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray hashes = new JsonArray(); req.put("phone_hash", hashes); - final String[] email_hashes = { + final String[] phoneHashes = { TokenUtils.getIdentityHashString("+15555555555"), TokenUtils.getIdentityHashString("+15555555556"), }; - for (String email_hash : email_hashes) { - hashes.add(email_hash); + for (String phoneHash : phoneHashes) { + hashes.add(phoneHash); } - send(vertx, apiVersion + "/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, email_hashes); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json, salt, false, IdentityType.Phone, true, phoneHashes); testContext.completeNow(); }); } @@ -2943,11 +3074,11 @@ 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(); + SaltEntry salt = setupSalts(); + JsonObject req = new JsonObject(); JsonArray phones = new JsonArray(); req.put("phone", phones); @@ -2956,8 +3087,8 @@ void identityMapBatchPhonesOnePhoneInvalid(Vertx vertx, VertxTestContext testCon phones.add("bogus"); phones.add("+15555555556"); - send(vertx, apiVersion + "/identity/map", req, 200, json -> { - checkIdentityMapResponse(json, "+15555555555", "+15555555556"); + send(vertx, "v2/identity/map", req, 200, json -> { + checkIdentityMapResponse(json, salt, false, IdentityType.Phone, false, "+15555555555", "+15555555556"); testContext.completeNow(); }); } @@ -2965,7 +3096,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(); @@ -2974,7 +3104,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(); }); @@ -2983,7 +3113,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(); @@ -2997,16 +3126,22 @@ 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 - @ValueSource(strings = {"policy", "optout_check"}) - void tokenGenerateRespectOptOutOption(String policyParameterKey, Vertx vertx, VertxTestContext testContext) { + @CsvSource({ + "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(); + setupSalts(useV4Uid); // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) @@ -3031,13 +3166,13 @@ 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; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + setupSalts(useV4Uid); // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) @@ -3048,7 +3183,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()); @@ -3063,18 +3198,19 @@ 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({ + "true,policy", + "true,optout_check", + + "false,policy", + "false,optout_check" + }) + void identityMapRespectOptOutOption(boolean useV4Uid, String policyParameterKey, Vertx vertx, VertxTestContext testContext) { final int clientSiteId = 201; - final String apiVersion = "v2"; fakeAuth(clientSiteId, Role.MAPPER); - setupSalts(); setupKeys(); + setupSalts(useV4Uid); // the clock value shouldn't matter here when(optOutStore.getLatestEntry(any(UserIdentity.class))) @@ -3086,7 +3222,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()); @@ -3102,7 +3238,6 @@ void identityMapRespectOptOutOption(String policyParameterKey, Vertx vertx, Vert @Test void requestWithoutClientKeyOrReferer(Vertx vertx, VertxTestContext testContext) { final String emailAddress = "test@uid2.com"; - final String apiVersion = "v2"; setupSalts(); setupKeys(); @@ -3113,7 +3248,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(); }); @@ -3341,7 +3476,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", ""); @@ -3430,7 +3564,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(); }); @@ -3457,7 +3590,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, @@ -3946,15 +4078,14 @@ 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) { identity.put("email_hash", getSha256(rawId)); } else if (identityType == IdentityType.Phone) { identity.put("phone_hash", getSha256(rawId)); - } else { //can't be other types - assertFalse(true); + } else { // can't be other types + org.junit.jupiter.api.Assertions.fail("Identity type is not: [email_hash,phone_hash]"); } return createClientSideTokenGenerateRequestWithPayload(identity, timestamp, appName); @@ -3965,22 +4096,33 @@ private Tuple.Tuple2 createClientSideTokenGenerateRequest return createClientSideTokenGenerateRequestWithPayload(identity, timestamp, null); } - @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 = setupSalts(useV4Uid); + 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, @@ -4000,12 +4142,14 @@ 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); + 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)); + setupSalts(useRefreshedV4Uid); + sendTokenRefresh(vertx, testContext, genBody.getString("refresh_token"), genBody.getString("refresh_response_key"), 200, refreshRespJson -> { assertEquals("optout", refreshRespJson.getString("status")); testContext.completeNow(); @@ -4014,26 +4158,49 @@ 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"); + SaltEntry salt = setupSalts(useV4Uid); + 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 + } else { when(optOutStore.getLatestEntry(any(UserIdentity.class))) .thenReturn(null); } @@ -4046,7 +4213,6 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id 200, testContext, respJson -> { - if (optOutExpected) { assertEquals("optout", respJson.getString("status")); testContext.completeNow(); @@ -4057,13 +4223,13 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id assertNotNull(genBody); decodeV2RefreshToken(respJson); - EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); RefreshToken refreshToken = decodeRefreshToken(encoder, genBody.getString("decrypted_refresh_token"), identityType); - 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); @@ -4074,22 +4240,23 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id TokenResponseStatsCollector.ResponseStatus.Success, TokenResponseStatsCollector.PlatformType.HasOriginHeader); + 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 -> { 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); + assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, refreshSalt, useRefreshedV4Uid, false), adTokenFromRefresh.userIdentity.id); String refreshTokenStringNew = refreshBody.getString("decrypted_refresh_token"); assertNotEquals(genRefreshToken, refreshTokenStringNew); RefreshToken refreshTokenAfterRefresh = decodeRefreshToken(encoder, refreshTokenStringNew, identityType); - 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); @@ -4114,7 +4281,9 @@ void cstgSuccessForBothOptedAndNonOptedOutTest(boolean optOutExpected, String id 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, @@ -4127,7 +4296,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); @@ -4140,7 +4308,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", @@ -4183,25 +4353,26 @@ 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, - 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, 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 = getAdvertisingIdFromIdentity(identityType, - identity, - firstLevelSalt, - rotatingSalt123.currentSalt()); + final byte[] advertisingId; + if (key == null) { + advertisingId = getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, rotatingSalt123.currentSalt()); + } else { + advertisingId = getAdvertisingIdFromIdentity(identityType, identityString, firstLevelSalt, key); + } - 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"), @@ -4218,6 +4389,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 * ********************************************************/ @@ -4440,7 +4621,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); @@ -4495,7 +4675,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 = { @@ -4527,7 +4706,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(); @@ -4578,12 +4757,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"); @@ -4609,7 +4785,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; @@ -4618,7 +4793,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); @@ -4628,7 +4803,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}; @@ -4677,7 +4851,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(); @@ -4714,7 +4887,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")); @@ -4749,17 +4922,16 @@ 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); } - String apiVersion = "v2"; int clientSiteId = 101; fakeAuth(clientSiteId, Role.SHARER); MultipleKeysetsTests test = new MultipleKeysetsTests(); @@ -4786,7 +4958,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")); @@ -4809,7 +4981,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 = { @@ -4823,7 +4994,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); @@ -4834,7 +5005,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; @@ -4875,7 +5045,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")); @@ -4929,7 +5099,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(); @@ -5018,7 +5187,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"); @@ -5058,11 +5227,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(); }); } @@ -5072,7 +5243,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(); @@ -5125,7 +5296,6 @@ void keySharingRespectsConfigValues(Vertx vertx, VertxTestContext testContext) { .withMaxBidstreamLifetimeSeconds(newMaxSharingLifetimeSeconds) .build(); - String apiVersion = "v2"; int siteId = 5; fakeAuth(siteId, Role.SHARER); Keyset[] keysets = { @@ -5138,7 +5308,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); @@ -5159,7 +5329,6 @@ void keyBidstreamRespectsConfigValues(String contentType, Vertx vertx, VertxTest .withMaxBidstreamLifetimeSeconds(newMaxBidstreamLifetimeSeconds) .build(); - final String apiVersion = "v2"; final KeyDownloadEndpoint endpoint = KeyDownloadEndpoint.BIDSTREAM; final int clientSiteId = 101; @@ -5168,7 +5337,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); @@ -5224,7 +5393,6 @@ void keySharingRespectsConfigValuesWithRemoteConfig(Vertx vertx, VertxTestContex .withMaxSharingLifetimeSeconds(newMaxSharingLifetimeSeconds) .build(); - String apiVersion = "v2"; int siteId = 5; fakeAuth(siteId, Role.SHARER); Keyset[] keysets = { @@ -5237,7 +5405,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); @@ -5257,7 +5425,6 @@ void keyBidstreamRespectsConfigValuesWithRemoteConfig(Vertx vertx, VertxTestCont .withMaxBidstreamLifetimeSeconds(newMaxBidstreamLifetimeSeconds) .build(); - final String apiVersion = "v2"; final KeyDownloadEndpoint endpoint = KeyDownloadEndpoint.BIDSTREAM; final int clientSiteId = 101; @@ -5266,7 +5433,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); @@ -5280,7 +5447,7 @@ private void assertLastUpdatedHasMillis(JsonArray buckets) { for (int i = 0; i < buckets.size(); i++) { JsonObject bucket = buckets.getJsonObject(i); String lastUpdated = bucket.getString("last_updated"); - // Verify pattern yyyy-MM-dd'T'HH:mm:ss.SSS + // Verify pattern yyyy-MM-dd'T'HH:mm:ss.SSS assertTrue(lastUpdated.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}"), "last_updated does not contain millisecond precision: " + lastUpdated); } diff --git a/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java b/src/test/java/com/uid2/operator/benchmark/BenchmarkCommon.java index 10565009b..dae5b88f4 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"))); @@ -86,7 +88,7 @@ static IUIDOperatorService createUidOperatorService() throws Exception { ); } - 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"))); @@ -100,7 +102,7 @@ static EncryptedTokenEncoder createTokenEncoder() throws Exception { return new EncryptedTokenEncoder(new KeyManager(keysetKeyStore, keysetProvider)); } - 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 +112,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 +150,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, + 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 +177,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 +192,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 +205,4 @@ public long getOptOutTimestampByAdId(String adId) { return -1; } } - } diff --git a/src/test/java/com/uid2/operator/benchmark/IdentityMapBenchmark.java b/src/test/java/com/uid2/operator/benchmark/IdentityMapBenchmark.java index e7bd5cd5d..a897755d9 100644 --- a/src/test/java/com/uid2/operator/benchmark/IdentityMapBenchmark.java +++ b/src/test/java/com/uid2/operator/benchmark/IdentityMapBenchmark.java @@ -59,7 +59,6 @@ public static class PayloadState { static byte[] nonce = com.uid2.shared.encryption.Random.getBytes(8); private static final Random RANDOM = new Random(); - @Setup public void setup() { this.payloadBinary = Buffer.buffer(createEncryptedPayload(this.numRecords)); 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) + ); + } +} 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..e9fc3b396 --- /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) 0b00100000; + 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) << 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]; + + 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); + } +}