3939import java .time .Instant ;
4040import java .time .format .DateTimeFormatter ;
4141import java .util .*;
42- import java .util .stream .Collectors ;
4342
4443import 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