99import com .uid2 .admin .vertx .RequestUtil ;
1010import com .uid2 .admin .vertx .ResponseUtil ;
1111import com .uid2 .admin .vertx .WriteLock ;
12+ import com .uid2 .client .Uid2Helper ;
1213import com .uid2 .shared .audit .AuditParams ;
1314import com .uid2 .shared .auth .Role ;
1415import com .uid2 .shared .model .SaltEntry ;
@@ -76,7 +77,7 @@ public class SaltService implements IService {
7677 private static final String OPERATOR_URL = "http://proxy:80" ;
7778 private static final Map <String , String > OPERATOR_HEADERS = Map .of ("Authorization" , String .format ("Bearer %s" , CLIENT_API_KEY ));
7879
79- private static final int IDENTITY_COUNT = 1_000 ;
80+ private static final int IDENTITY_COUNT = 10_000 ;
8081 private static final int TIMESTAMP_LENGTH = 8 ;
8182 private static final int IV_LENGTH = 12 ;
8283
@@ -115,6 +116,12 @@ public void setupRoutes(Router router) {
115116 }
116117 }, new AuditParams (List .of ("fraction" ), Collections .emptyList ()), Role .MAINTAINER , Role .SECRET_ROTATION ));
117118
119+ router .post ("/api/salt/compare" ).blockingHandler (auth .handle (ctx -> {
120+ synchronized (writeLock ) {
121+ this .handleSaltCompare (ctx );
122+ }
123+ }, new AuditParams (List .of (), Collections .emptyList ()), Role .MAINTAINER , Role .SECRET_ROTATION ));
124+
118125 router .post ("/api/salt/simulateToken" ).blockingHandler (auth .handle (ctx -> {
119126 synchronized (writeLock ) {
120127 this .handleSaltSimulateToken (ctx );
@@ -260,6 +267,99 @@ private void handleSaltFastForward(RoutingContext rc) {
260267 }
261268 }
262269
270+ private void handleSaltCompare (RoutingContext rc ) {
271+ try {
272+ final String testOperatorUrl = RequestUtil .getString (rc , "test_operator_url" ).orElse ("" );
273+ final String testApiKey = RequestUtil .getString (rc , "test_api_key" ).orElse ("" );
274+ final String testApiSecret = RequestUtil .getString (rc , "test_api_secret" ).orElse ("" );
275+ final String integOperatorUrl = RequestUtil .getString (rc , "integ_operator_url" ).orElse ("" );
276+ final String integApiKey = RequestUtil .getString (rc , "integ_api_key" ).orElse ("" );
277+ final String integApiSecret = RequestUtil .getString (rc , "integ_api_secret" ).orElse ("" );
278+
279+ List <String > emails = new ArrayList <>();
280+ Map <String , Boolean > emailToSaltMap = new HashMap <>();
281+ RotatingSaltProvider .SaltSnapshot snapshot = saltProvider .getSnapshots ().getLast ();
282+ for (int i = 0 ; i < IDENTITY_COUNT ; i ++) {
283+ String email = randomEmail ();
284+ emails .add (email );
285+
286+ SaltEntry salt = getSalt (email , snapshot );
287+ boolean isV4 = salt .currentKeySalt () != null && salt .currentKeySalt ().key () != null && salt .currentKeySalt ().salt () != null ;
288+ emailToSaltMap .put (email , isV4 );
289+ }
290+
291+ // Construct identity map args
292+ JsonNode testResponse = v3IdentityMap (emails , testOperatorUrl , testApiKey , testApiSecret );
293+ JsonNode integResponse = v3IdentityMap (emails , integOperatorUrl , integApiKey , integApiSecret );
294+
295+ JsonNode testMappings = testResponse .at ("/body/email" );
296+ JsonNode integMappings = integResponse .at ("/body/email" );
297+
298+ int testV4UidCount = 0 ;
299+ int testV2UidCount = 0 ;
300+ int testInvalidV4UidCount = 0 ;
301+ int testInvalidV2UidCount = 0 ;
302+ int testNullUidCount = 0 ;
303+ int testIntegMatchCount = 0 ;
304+ int testIntegMismatchCount = 0 ;
305+ for (int i = 0 ; i < IDENTITY_COUNT ; i ++) {
306+ String email = emails .get (i );
307+ boolean isV4 = emailToSaltMap .get (email );
308+
309+ String testUid = testMappings .get (i ).at ("/u" ).asText ();
310+ String integUid = integMappings .get (i ).at ("/u" ).asText ();
311+
312+ byte [] testUidBytes = testUid == null ? null : Base64 .getDecoder ().decode (testUid );
313+ byte [] integUidBytes = integUid == null ? null : Base64 .getDecoder ().decode (integUid );
314+
315+ // First, check test UID is valid
316+ if (testUidBytes == null ) {
317+ LOGGER .error ("TEST - UID is null" );
318+ testNullUidCount ++;
319+ } else if (isV4 ) {
320+ if (testUidBytes .length != 33 ) {
321+ LOGGER .error ("TEST - salt is v4 but UID length is {}" , testUidBytes .length );
322+ testInvalidV4UidCount ++;
323+ } else {
324+ testV4UidCount ++;
325+ }
326+ } else {
327+ if (testUidBytes .length != 32 ) {
328+ LOGGER .error ("TEST - salt is v2 but UID length is {}" , testUidBytes .length );
329+ testInvalidV2UidCount ++;
330+ } else {
331+ testV2UidCount ++;
332+ }
333+ }
334+
335+ // Then, check if test and integ match
336+ if (!isV4 ) {
337+ if (!Arrays .equals (testUidBytes , integUidBytes )) {
338+ LOGGER .error ("TEST and INTEG UIDs do not match for {} - TEST={} | INTEG={}" , email , testUid , integUid );
339+ testIntegMismatchCount ++;
340+ } else {
341+ testIntegMatchCount ++;
342+ }
343+ }
344+ }
345+
346+ LOGGER .info ("UID Consistency between Test and Integ: " +
347+ "test_v4_uid_count={} test_v2_uid_count={} " +
348+ "test_v4_invalid_uid_count={} test_v2_invalid_uid_count={} test_null_uid_count={} " +
349+ "test_integ_match_count={} test_integ_mismatch_count={}" ,
350+ testV4UidCount , testV2UidCount ,
351+ testInvalidV4UidCount , testInvalidV2UidCount , testNullUidCount ,
352+ testIntegMatchCount , testIntegMismatchCount );
353+
354+ rc .response ()
355+ .putHeader (HttpHeaders .CONTENT_TYPE , "application/json" )
356+ .end ();
357+ } catch (Exception e ) {
358+ LOGGER .error (e .getMessage (), e );
359+ rc .fail (500 , e );
360+ }
361+ }
362+
263363 private void handleSaltSimulateToken (RoutingContext rc ) {
264364 try {
265365 final double fraction = RequestUtil .getDouble (rc , "fraction" ).orElse (0.002740 );
@@ -329,8 +429,8 @@ private void handleSaltSimulateToken(RoutingContext rc) {
329429 }
330430
331431 LOGGER .info (
332- "UID token simulation: success_count={}, failure_count={}, " +
333- "v2_to_v4_count={}, v4_to_v4_count={}, v4_to_v2_count={}, v2_to_v2_count={}" ,
432+ "UID token simulation: success_count={} failure_count={} " +
433+ "v2_to_v4_count={} v4_to_v4_count={} v4_to_v2_count={} v2_to_v2_count={}" ,
334434 emailToRefreshSuccessMap .values ().stream ().filter (x -> x ).count (),
335435 emailToRefreshSuccessMap .values ().stream ().filter (x -> !x ).count (),
336436 emails .stream ().filter (email -> !preRotationEmailToSaltMap .get (email ) && postRotationEmailToSaltMap .get (email )).count (),
@@ -782,7 +882,7 @@ private void v2LoadSalts() throws Exception {
782882 HTTP_CLIENT .post (String .format ("%s/v2/salts/load" , OPERATOR_URL ), "" , OPERATOR_HEADERS );
783883 }
784884
785- private JsonNode v3IdentityMap (List <String > emails ) throws Exception {
885+ private JsonNode v3IdentityMap (List <String > emails , String baseUrl , String key , String secret ) throws Exception {
786886 StringBuilder reqBody = new StringBuilder ("{ \" email\" : [" );
787887 for (String email : emails ) {
788888 reqBody .append (String .format ("\" %s\" " , email ));
@@ -792,9 +892,14 @@ private JsonNode v3IdentityMap(List<String> emails) throws Exception {
792892 }
793893 reqBody .append ("] }" );
794894
795- V2Envelope envelope = v2CreateEnvelope (reqBody .toString (), CLIENT_API_SECRET );
796- HttpResponse <String > response = HTTP_CLIENT .post (String .format ("%s/v3/identity/map" , OPERATOR_URL ), envelope .envelope (), OPERATOR_HEADERS );
797- return v2DecryptEncryptedResponse (response .body (), envelope .nonce (), CLIENT_API_SECRET );
895+ V2Envelope envelope = v2CreateEnvelope (reqBody .toString (), secret );
896+ Map <String , String > headers = Map .of ("Authorization" , String .format ("Bearer %s" , key ));
897+ HttpResponse <String > response = HTTP_CLIENT .post (String .format ("%s/v3/identity/map" , baseUrl ), envelope .envelope (), headers );
898+ return v2DecryptEncryptedResponse (response .body (), envelope .nonce (), secret );
899+ }
900+
901+ private JsonNode v3IdentityMap (List <String > emails ) throws Exception {
902+ return v3IdentityMap (emails , OPERATOR_URL , CLIENT_API_KEY , CLIENT_API_SECRET );
798903 }
799904
800905 private JsonNode v2TokenGenerate (String email ) throws Exception {
0 commit comments