Skip to content

Commit 83014cd

Browse files
committed
Added /token/generate and /token/refresh tests
1 parent f3b6b98 commit 83014cd

File tree

2 files changed

+151
-10
lines changed

2 files changed

+151
-10
lines changed

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

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

232308
RotatingSaltProvider.SaltSnapshot firstSnapshot = saltProvider.getSnapshots().getLast();
233-
SaltRotation.Result result = null;
234309
List<String> emails = new ArrayList<>();
235310
Map<String, Map<Long, Map<String, String>>> emailToUidMapping = new HashMap<>();
236311
for (int j = 0; j < IDENTITY_COUNT; j++) {
@@ -264,7 +339,7 @@ private void handleSaltSimulate(RoutingContext rc) {
264339

265340
rc.response()
266341
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
267-
.end(toJson(result.getSnapshot()).encode());
342+
.end();
268343
} catch (Exception e) {
269344
LOGGER.error(e.getMessage(), e);
270345
rc.fail(500, e);
@@ -416,7 +491,7 @@ private boolean assertSaltState(
416491
LOGGER.error("Invalid salt state - V4 UID enabled but missing key on rotated salt");
417492
return false;
418493
} else if (rotated && !enabledV4Uid && missingSalt) {
419-
LOGGER.error("Invalid salt state - V4 UID enabled but missing salt on rotated salt");
494+
LOGGER.error("Invalid salt state - V4 UID disabled but missing salt on rotated salt");
420495
return false;
421496
}
422497

@@ -578,10 +653,18 @@ private byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte me
578653
ivBase.write(salt.getBytes());
579654
ivBase.write(firstLevelHashLast16Bytes);
580655
ivBase.write(metadata);
581-
ivBase.write(keyId);
656+
ivBase.write(getKeyIdBytes(keyId));
582657
return Arrays.copyOfRange(getSha256Bytes(ivBase.toByteArray(), null), 0, IV_LENGTH);
583658
}
584659

660+
private byte[] getKeyIdBytes(int keyId) {
661+
return new byte[] {
662+
(byte) ((keyId >> 16) & 0xFF), // MSB
663+
(byte) ((keyId >> 8) & 0xFF), // Middle
664+
(byte) (keyId & 0xFF), // LSB
665+
};
666+
}
667+
585668
private byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception {
586669
// Set up AES256-CTR cipher
587670
Cipher aesCtr = Cipher.getInstance("AES/CTR/NoPadding");
@@ -655,7 +738,20 @@ private JsonNode v3IdentityMap(List<String> emails) throws Exception {
655738
return v2DecryptEncryptedResponse(response.body(), envelope.nonce(), CLIENT_API_SECRET);
656739
}
657740

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

@@ -693,18 +789,36 @@ private JsonNode v2DecryptEncryptedResponse(String encryptedResponse, byte[] non
693789
return OBJECT_MAPPER.readTree(decryptedResponse);
694790
}
695791

792+
private JsonNode v2DecryptRefreshResponse(String response, String refreshResponseKey) throws Exception {
793+
if (response.contains("client_error")) {
794+
return null;
795+
}
796+
797+
byte[] encryptedResponseBytes = base64ToByteArray(response);
798+
byte[] refreshResponseKeyBytes = base64ToByteArray(refreshResponseKey);
799+
byte[] payload = decryptGCM(encryptedResponseBytes, refreshResponseKeyBytes);
800+
return OBJECT_MAPPER.readTree(new String(payload, StandardCharsets.UTF_8));
801+
}
802+
696803
private byte[] encryptGDM(byte[] b, byte[] secretBytes) throws Exception {
697804
Class<?> clazz = Class.forName("com.uid2.client.Uid2Encryption");
698805
Method encryptGDMMethod = clazz.getDeclaredMethod("encryptGCM", byte[].class, byte[].class, byte[].class);
699806
encryptGDMMethod.setAccessible(true);
700807
return (byte[]) encryptGDMMethod.invoke(clazz, b, null, secretBytes);
701808
}
702809

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

707-
private static String byteArrayToBase64(byte[] b) {
821+
private String byteArrayToBase64(byte[] b) {
708822
return Base64.getEncoder().encodeToString(b);
709823
}
710824

webroot/adm/salt.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ <h1>UID2 Env - Salt Management</h1>
4141
defaultValue: defaultTargetDate
4242
};
4343

44+
const tokenIterationsInput = {
45+
name: 'tokenIterations',
46+
label: 'Token Iterations',
47+
required: true,
48+
type: 'number',
49+
defaultValue: 0
50+
};
51+
4452
const preMigrationIterationsInput = {
4553
name: 'preMigrationIterations',
4654
label: 'Step 1 - Pre-Migration Iterations',
@@ -89,6 +97,25 @@ <h1>UID2 Env - Salt Management</h1>
8997
url: '/api/salt/rebuild'
9098
}
9199
},
100+
{
101+
id: 'simulateToken',
102+
title: 'Simulate Token',
103+
role: 'maintainer',
104+
inputs: [
105+
fractionInput,
106+
targetDateInput,
107+
tokenIterationsInput
108+
],
109+
apiCall: {
110+
method: 'POST',
111+
getUrl: (inputs) => {
112+
const fraction = encodeURIComponent(inputs.fraction);
113+
const targetDate = encodeURIComponent(inputs.targetDate);
114+
const iterations = encodeURIComponent(inputs.tokenIterations);
115+
return `/api/salt/simulate/token?fraction=${fraction}&target_date=${targetDate}&iterations=${iterations}`;
116+
}
117+
},
118+
},
92119
{
93120
id: 'simulateRotation',
94121
title: 'Simulate Rotation',

0 commit comments

Comments
 (0)