Skip to content

Commit 6973df7

Browse files
committed
Added /token/generate and /token/refresh tests
1 parent f3b6b98 commit 6973df7

File tree

1 file changed

+122
-10
lines changed

1 file changed

+122
-10
lines changed

src/main/java/com/uid2/admin/vertx/service/SaltService.java

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import java.time.Instant;
4040
import java.time.format.DateTimeFormatter;
4141
import java.util.*;
42-
import java.util.stream.Collectors;
4342

4443
import static com.uid2.admin.vertx.Endpoints.*;
4544

@@ -111,11 +110,17 @@ public void setupRoutes(Router router) {
111110
}
112111
}, new AuditParams(List.of("fraction", "target_date"), Collections.emptyList()), Role.SUPER_USER, Role.SECRET_ROTATION));
113112

113+
router.post("/api/salt/simulate/token").blockingHandler(auth.handle((ctx) -> {
114+
synchronized (writeLock) {
115+
this.handleSaltSimulateToken(ctx);
116+
}
117+
}, new AuditParams(List.of("target_date"), Collections.emptyList()), Role.MAINTAINER, Role.SECRET_ROTATION));
118+
114119
router.post("/api/salt/simulate").blockingHandler(auth.handle((ctx) -> {
115120
synchronized (writeLock) {
116121
this.handleSaltSimulate(ctx);
117122
}
118-
}, new AuditParams(List.of("fraction", "target_date"), Collections.emptyList()), Role.SUPER_USER, Role.SECRET_ROTATION));
123+
}, new AuditParams(List.of("fraction", "target_date"), Collections.emptyList()), Role.MAINTAINER, Role.SECRET_ROTATION));
119124
}
120125

121126
private void handleSaltSnapshots(RoutingContext rc) {
@@ -169,7 +174,7 @@ private void handleSaltRotate(RoutingContext rc) {
169174
final Optional<Double> fraction = RequestUtil.getDouble(rc, "fraction");
170175
if (fraction.isEmpty()) return;
171176

172-
LOGGER.info("Salt rotation age thresholds in seconds: {}", Arrays.stream(SALT_ROTATION_AGE_THRESHOLDS).map(Duration::toSeconds).collect(Collectors.toList()));
177+
LOGGER.info("Salt rotation age thresholds in seconds: {}", Arrays.stream(SALT_ROTATION_AGE_THRESHOLDS).map(Duration::toSeconds).toList());
173178

174179
final TargetDate targetDate =
175180
RequestUtil.getDate(rc, "target_date", DateTimeFormatter.ISO_LOCAL_DATE)
@@ -216,6 +221,75 @@ private JsonObject toJson(RotatingSaltProvider.SaltSnapshot snapshot) {
216221
return jo;
217222
}
218223

224+
private void handleSaltSimulateToken(RoutingContext rc) {
225+
try {
226+
final double fraction = RequestUtil.getDouble(rc, "fraction").orElse(0.002740);
227+
228+
TargetDate targetDate =
229+
RequestUtil.getDate(rc, "target_date", DateTimeFormatter.ISO_LOCAL_DATE)
230+
.map(TargetDate::new)
231+
.orElse(TargetDate.now().plusDays(1));
232+
233+
// Step 1. Run /v2/token/generate for 10,000 emails
234+
List<String> emails = new ArrayList<>();
235+
for (int j = 0; j < 3; j++) {
236+
String email = randomEmail();
237+
emails.add(email);
238+
}
239+
240+
Map<String, String> emailToRefreshTokenMap = new HashMap<>();
241+
Map<String, String> emailToRefreshResponseKeyMap = new HashMap<>();
242+
for (String email : emails) {
243+
JsonNode tokens = v2TokenGenerate(email);
244+
String refreshToken = tokens.at("/body/refresh_token").asText();
245+
String refreshResponseKey = tokens.at("/body/refresh_response_key").asText();
246+
247+
emailToRefreshTokenMap.put(email, refreshToken);
248+
emailToRefreshResponseKeyMap.put(email, refreshResponseKey);
249+
}
250+
251+
// Step 2. Rotate salts 30 times
252+
RotatingSaltProvider.SaltSnapshot snapshot = null;
253+
for (int i = 0; i < 30; i++) {
254+
snapshot = rotateSalts(rc, fraction, targetDate, i);
255+
}
256+
257+
// Step 3. Count how many emails are v2 vs v4 salts
258+
Map<String, Boolean> emailToV4TokenMap = new HashMap<>();
259+
for (String email : emails) {
260+
SaltEntry salt = getSalt(email, snapshot);
261+
boolean isV4 = salt.currentKeySalt() != null && salt.currentKeySalt().key() != null && salt.currentKeySalt().salt() != null;
262+
emailToV4TokenMap.put(email, isV4);
263+
}
264+
265+
// Step 4. Run /v2/token/refresh for all emails
266+
Map<String, Boolean> emailToRefreshSuccessMap = new HashMap<>();
267+
for (String email : emails) {
268+
try {
269+
JsonNode response = v2TokenRefresh(emailToRefreshTokenMap.get(email), emailToRefreshResponseKeyMap.get(email));
270+
emailToRefreshSuccessMap.put(email, response != null);
271+
} catch (Exception e) {
272+
LOGGER.error(e.getMessage(), e);
273+
emailToRefreshSuccessMap.put(email, false);
274+
}
275+
}
276+
277+
LOGGER.info(
278+
"UID token simulation: success_count={}, failure_count={}, v4_count={}, v2_count={}",
279+
emailToRefreshSuccessMap.values().stream().filter(x -> x).count(),
280+
emailToRefreshSuccessMap.values().stream().filter(x -> !x).count(),
281+
emailToV4TokenMap.values().stream().filter(x -> x).count(),
282+
emailToV4TokenMap.values().stream().filter(x -> !x).count());
283+
284+
rc.response()
285+
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
286+
.end();
287+
} catch (Exception e) {
288+
LOGGER.error(e.getMessage(), e);
289+
rc.fail(500, e);
290+
}
291+
}
292+
219293
private void handleSaltSimulate(RoutingContext rc) {
220294
try {
221295
final double fraction = RequestUtil.getDouble(rc, "fraction").orElse(0.002740);
@@ -230,7 +304,6 @@ private void handleSaltSimulate(RoutingContext rc) {
230304
.orElse(TargetDate.now().plusDays(1));
231305

232306
RotatingSaltProvider.SaltSnapshot firstSnapshot = saltProvider.getSnapshots().getLast();
233-
SaltRotation.Result result = null;
234307
List<String> emails = new ArrayList<>();
235308
Map<String, Map<Long, Map<String, String>>> emailToUidMapping = new HashMap<>();
236309
for (int j = 0; j < IDENTITY_COUNT; j++) {
@@ -264,7 +337,7 @@ private void handleSaltSimulate(RoutingContext rc) {
264337

265338
rc.response()
266339
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
267-
.end(toJson(result.getSnapshot()).encode());
340+
.end();
268341
} catch (Exception e) {
269342
LOGGER.error(e.getMessage(), e);
270343
rc.fail(500, e);
@@ -416,7 +489,7 @@ private boolean assertSaltState(
416489
LOGGER.error("Invalid salt state - V4 UID enabled but missing key on rotated salt");
417490
return false;
418491
} else if (rotated && !enabledV4Uid && missingSalt) {
419-
LOGGER.error("Invalid salt state - V4 UID enabled but missing salt on rotated salt");
492+
LOGGER.error("Invalid salt state - V4 UID disabled but missing salt on rotated salt");
420493
return false;
421494
}
422495

@@ -578,10 +651,18 @@ private byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte me
578651
ivBase.write(salt.getBytes());
579652
ivBase.write(firstLevelHashLast16Bytes);
580653
ivBase.write(metadata);
581-
ivBase.write(keyId);
654+
ivBase.write(getKeyIdBytes(keyId));
582655
return Arrays.copyOfRange(getSha256Bytes(ivBase.toByteArray(), null), 0, IV_LENGTH);
583656
}
584657

658+
private byte[] getKeyIdBytes(int keyId) {
659+
return new byte[] {
660+
(byte) ((keyId >> 16) & 0xFF), // MSB
661+
(byte) ((keyId >> 8) & 0xFF), // Middle
662+
(byte) (keyId & 0xFF), // LSB
663+
};
664+
}
665+
585666
private byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception {
586667
// Set up AES256-CTR cipher
587668
Cipher aesCtr = Cipher.getInstance("AES/CTR/NoPadding");
@@ -655,7 +736,20 @@ private JsonNode v3IdentityMap(List<String> emails) throws Exception {
655736
return v2DecryptEncryptedResponse(response.body(), envelope.nonce(), CLIENT_API_SECRET);
656737
}
657738

658-
private static String randomEmail() {
739+
private JsonNode v2TokenGenerate(String email) throws Exception {
740+
String reqBody = String.format("{ \"email\": \"%s\", \"optout_check\": 1}", email);
741+
742+
V2Envelope envelope = v2CreateEnvelope(reqBody, CLIENT_API_SECRET);
743+
HttpResponse<String> response = HTTP_CLIENT.post(String.format("%s/v2/token/generate", OPERATOR_URL), envelope.envelope(), OPERATOR_HEADERS);
744+
return v2DecryptEncryptedResponse(response.body(), envelope.nonce(), CLIENT_API_SECRET);
745+
}
746+
747+
private JsonNode v2TokenRefresh(String refreshToken, String refreshResponseKey) throws Exception {
748+
HttpResponse<String> response = HTTP_CLIENT.post(String.format("%s/v2/token/refresh", OPERATOR_URL), refreshToken, OPERATOR_HEADERS);
749+
return v2DecryptRefreshResponse(response.body(), refreshResponseKey);
750+
}
751+
752+
private String randomEmail() {
659753
return "email_" + Math.abs(SECURE_RANDOM.nextLong()) + "@example.com";
660754
}
661755

@@ -693,18 +787,36 @@ private JsonNode v2DecryptEncryptedResponse(String encryptedResponse, byte[] non
693787
return OBJECT_MAPPER.readTree(decryptedResponse);
694788
}
695789

790+
private JsonNode v2DecryptRefreshResponse(String response, String refreshResponseKey) throws Exception {
791+
if (response.contains("client_error")) {
792+
return null;
793+
}
794+
795+
byte[] encryptedResponseBytes = base64ToByteArray(response);
796+
byte[] refreshResponseKeyBytes = base64ToByteArray(refreshResponseKey);
797+
byte[] payload = decryptGCM(encryptedResponseBytes, refreshResponseKeyBytes);
798+
return OBJECT_MAPPER.readTree(new String(payload, StandardCharsets.UTF_8));
799+
}
800+
696801
private byte[] encryptGDM(byte[] b, byte[] secretBytes) throws Exception {
697802
Class<?> clazz = Class.forName("com.uid2.client.Uid2Encryption");
698803
Method encryptGDMMethod = clazz.getDeclaredMethod("encryptGCM", byte[].class, byte[].class, byte[].class);
699804
encryptGDMMethod.setAccessible(true);
700805
return (byte[]) encryptGDMMethod.invoke(clazz, b, null, secretBytes);
701806
}
702807

703-
private static byte[] base64ToByteArray(String str) {
808+
private byte[] decryptGCM(byte[] b, byte[] secretBytes) throws Exception {
809+
Class<?> clazz = Class.forName("com.uid2.client.Uid2Encryption");
810+
Method decryptGCMMethod = clazz.getDeclaredMethod("decryptGCM", byte[].class, int.class, byte[].class);
811+
decryptGCMMethod.setAccessible(true);
812+
return (byte[]) decryptGCMMethod.invoke(clazz, b, 0, secretBytes);
813+
}
814+
815+
private byte[] base64ToByteArray(String str) {
704816
return Base64.getDecoder().decode(str);
705817
}
706818

707-
private static String byteArrayToBase64(byte[] b) {
819+
private String byteArrayToBase64(byte[] b) {
708820
return Base64.getEncoder().encodeToString(b);
709821
}
710822

0 commit comments

Comments
 (0)