44import com .uid2 .operator .util .Tuple ;
55import com .uid2 .operator .vertx .ClientInputValidationException ;
66import com .uid2 .shared .Const .Data ;
7- import com .uid2 .shared .encryption .AesCbc ;
87import com .uid2 .shared .encryption .AesGcm ;
98import com .uid2 .shared .encryption .Uid2Base64UrlCoder ;
109import com .uid2 .shared .model .KeysetKey ;
1312import io .micrometer .core .instrument .Counter ;
1413import io .micrometer .core .instrument .Metrics ;
1514
15+ import java .nio .charset .StandardCharsets ;
1616import java .time .Instant ;
1717import java .util .Base64 ;
1818import java .util .HashMap ;
1919import java .util .Map ;
2020
21+ import io .vertx .core .json .JsonObject ;
22+
2123public class EncryptedTokenEncoder implements ITokenEncoder {
2224 private final KeyManager keyManager ;
2325 private final Map <Tuple .Tuple2 <String , String >, Counter > siteKeysetStatusMetrics = new HashMap <>();
@@ -29,27 +31,7 @@ public EncryptedTokenEncoder(KeyManager keyManager) {
2931 public byte [] encode (AdvertisingToken t , Instant asOf ) {
3032 final KeysetKey masterKey = this .keyManager .getMasterKey (asOf );
3133 final KeysetKey siteEncryptionKey = this .keyManager .getActiveKeyBySiteIdWithFallback (t .publisherIdentity .siteId , Data .AdvertisingTokenSiteId , asOf , siteKeysetStatusMetrics );
32-
33- return t .version == TokenVersion .V2
34- ? encodeV2 (t , masterKey , siteEncryptionKey )
35- : encodeV3 (t , masterKey , siteEncryptionKey ); //TokenVersion.V4 also calls encodeV3() since the byte array is identical between V3 and V4
36- }
37-
38- private byte [] encodeV2 (AdvertisingToken t , KeysetKey masterKey , KeysetKey siteKey ) {
39- final Buffer b = Buffer .buffer ();
40-
41- b .appendByte ((byte ) t .version .rawVersion );
42- b .appendInt (masterKey .getId ());
43-
44- Buffer b2 = Buffer .buffer ();
45- b2 .appendLong (t .expiresAt .toEpochMilli ());
46- encodeSiteIdentityV2 (b2 , t .publisherIdentity , t .userIdentity , siteKey );
47-
48- final byte [] encryptedId = AesCbc .encrypt (b2 .getBytes (), masterKey ).getPayload ();
49-
50- b .appendBytes (encryptedId );
51-
52- return b .getBytes ();
34+ return encodeV3 (t , masterKey , siteEncryptionKey ); // TokenVersion.V4 calls encodeV3() since the byte array is identical between V3 and V4
5335 }
5436
5537 private byte [] encodeV3 (AdvertisingToken t , KeysetKey masterKey , KeysetKey siteKey ) {
@@ -86,52 +68,40 @@ public RefreshToken decodeRefreshToken(String s) {
8668 throw new ClientInputValidationException ("Invalid refresh token" );
8769 }
8870 final Buffer b = Buffer .buffer (bytes );
89- if (b .getByte (1 ) == TokenVersion .V3 .rawVersion ) {
90- return decodeRefreshTokenV3 (b , bytes );
91- } else if (b .getByte (0 ) == TokenVersion .V2 .rawVersion ) {
92- return decodeRefreshTokenV2 (b );
71+ if (bytes .length >= 6 && (b .getByte (1 ) & 0xff ) == TokenVersion .V4 .rawVersion ) {
72+ return decodeRefreshTokenV4 (b , bytes );
9373 }
9474 }
9575
9676 throw new ClientInputValidationException ("Invalid refresh token version" );
9777 }
9878
99- private RefreshToken decodeRefreshTokenV2 (Buffer b ) {
100- final Instant createdAt = Instant .ofEpochMilli (b .getLong (1 ));
101- //final Instant expiresAt = Instant.ofEpochMilli(b.getLong(9));
102- final Instant validTill = Instant .ofEpochMilli (b .getLong (17 ));
103- final int keyId = b .getInt (25 );
104-
105- final KeysetKey key = this .keyManager .getKey (keyId );
106-
79+ /**
80+ * Unwraps the V2 API refresh token envelope (scope + keyId + encrypted JSON) and returns
81+ * the inner refresh token string. Used when the response body contains refresh_response_key
82+ * and refresh_token is this wrapped form rather than the raw V4 token.
83+ */
84+ public String unwrapV2RefreshEnvelope (String wrappedBase64 ) {
85+ byte [] bytes = EncodingUtils .fromBase64 (wrappedBase64 );
86+ if (bytes .length < 5 ) {
87+ throw new ClientInputValidationException ("Invalid V2 refresh envelope" );
88+ }
89+ Buffer b = Buffer .buffer (bytes );
90+ int keyId = b .getInt (1 );
91+ KeysetKey key = this .keyManager .getKey (keyId );
10792 if (key == null ) {
10893 throw new ClientInputValidationException ("Failed to fetch key with id: " + keyId );
10994 }
110-
111- final byte [] decryptedPayload = AesCbc .decrypt (b .slice (29 , b .length ()).getBytes (), key );
112-
113- final Buffer b2 = Buffer .buffer (decryptedPayload );
114-
115- final int siteId = b2 .getInt (0 );
116- final int length = b2 .getInt (4 );
117- final byte [] identity ;
118- try {
119- identity = EncodingUtils .fromBase64 (b2 .slice (8 , 8 + length ).getBytes ());
120- } catch (Exception e ) {
121- throw new ClientInputValidationException ("Failed to decode refreshTokenV2: Identity segment is not valid base64." , e );
95+ byte [] decrypted = AesGcm .decrypt (bytes , 5 , key );
96+ JsonObject json = new JsonObject (new String (decrypted , StandardCharsets .UTF_8 ));
97+ String innerToken = json .getString ("refresh_token" );
98+ if (innerToken == null ) {
99+ throw new ClientInputValidationException ("V2 refresh envelope missing refresh_token" );
122100 }
123-
124- final int privacyBits = b2 .getInt (8 + length );
125- final long establishedMillis = b2 .getLong (8 + length + 4 );
126-
127- return new RefreshToken (
128- TokenVersion .V2 , createdAt , validTill ,
129- new OperatorIdentity (0 , OperatorType .Service , 0 , 0 ),
130- new PublisherIdentity (siteId , 0 , 0 ),
131- new UserIdentity (IdentityScope .UID2 , IdentityType .Email , identity , privacyBits , Instant .ofEpochMilli (establishedMillis ), null ));
101+ return innerToken ;
132102 }
133103
134- private RefreshToken decodeRefreshTokenV3 (Buffer b , byte [] bytes ) {
104+ private RefreshToken decodeRefreshTokenV4 (Buffer b , byte [] bytes ) {
135105 final int keyId = b .getInt (2 );
136106 final KeysetKey key = this .keyManager .getKey (keyId );
137107
@@ -153,20 +123,19 @@ private RefreshToken decodeRefreshTokenV3(Buffer b, byte[] bytes) {
153123 final byte [] id = b2 .getBytes (58 , 90 );
154124
155125 if (identityScope != decodeIdentityScopeV3 (b .getByte (0 ))) {
156- throw new ClientInputValidationException ("Failed to decode refreshTokenV3 : Identity scope mismatch" );
126+ throw new ClientInputValidationException ("Failed to decode refreshTokenV4 : Identity scope mismatch" );
157127 }
158128 if (identityType != decodeIdentityTypeV3 (b .getByte (0 ))) {
159- throw new ClientInputValidationException ("Failed to decode refreshTokenV3 : Identity type mismatch" );
129+ throw new ClientInputValidationException ("Failed to decode refreshTokenV4 : Identity type mismatch" );
160130 }
161131
162132 return new RefreshToken (
163- TokenVersion .V3 , createdAt , expiresAt , operatorIdentity , publisherIdentity ,
133+ TokenVersion .V4 , createdAt , expiresAt , operatorIdentity , publisherIdentity ,
164134 new UserIdentity (identityScope , identityType , id , privacyBits , establishedAt , null ));
165135 }
166136
167137 @ Override
168138 public AdvertisingToken decodeAdvertisingToken (String base64AdvertisingToken ) {
169- //Logic and code copied from: https://github.com/IABTechLab/uid2-client-java/blob/0220ef43c1661ecf3b8f4ed2db524e2db31c06b5/src/main/java/com/uid2/client/Uid2Encryption.java#L37
170139 if (base64AdvertisingToken .length () < 4 ) {
171140 throw new ClientInputValidationException ("Advertising token is too short" );
172141 }
@@ -175,67 +144,14 @@ public AdvertisingToken decodeAdvertisingToken(String base64AdvertisingToken) {
175144 boolean isBase64UrlEncoding = (headerStr .indexOf ('-' ) != -1 || headerStr .indexOf ('_' ) != -1 );
176145 byte [] headerBytes = isBase64UrlEncoding ? Uid2Base64UrlCoder .decode (headerStr ) : Base64 .getDecoder ().decode (headerStr );
177146
178- if (headerBytes [0 ] == TokenVersion .V2 .rawVersion ) {
179- final byte [] bytes = EncodingUtils .fromBase64 (base64AdvertisingToken );
180- final Buffer b = Buffer .buffer (bytes );
181- return decodeAdvertisingTokenV2 (b );
182- }
183-
184- //Java's byte is signed, so we convert to unsigned before checking the enum
185147 int unsignedByte = ((int ) headerBytes [1 ]) & 0xff ;
186-
187- byte [] bytes ;
188- TokenVersion tokenVersion ;
189- if (unsignedByte == TokenVersion .V3 .rawVersion ) {
190- bytes = EncodingUtils .fromBase64 (base64AdvertisingToken );
191- tokenVersion = TokenVersion .V3 ;
192- } else if (unsignedByte == TokenVersion .V4 .rawVersion ) {
193- bytes = Uid2Base64UrlCoder .decode (base64AdvertisingToken ); //same as V3 but use Base64URL encoding
194- tokenVersion = TokenVersion .V4 ;
195- } else {
196- throw new ClientInputValidationException ("Invalid advertising token version" );
148+ if (unsignedByte != TokenVersion .V4 .rawVersion ) {
149+ throw new ClientInputValidationException ("V2/V3 advertising token no longer supported" );
197150 }
198151
152+ final byte [] bytes = Uid2Base64UrlCoder .decode (base64AdvertisingToken );
199153 final Buffer b = Buffer .buffer (bytes );
200- return decodeAdvertisingTokenV3orV4 (b , bytes , tokenVersion );
201- }
202-
203- public AdvertisingToken decodeAdvertisingTokenV2 (Buffer b ) {
204- try {
205- final int masterKeyId = b .getInt (1 );
206-
207- final byte [] decryptedPayload = AesCbc .decrypt (b .slice (5 , b .length ()).getBytes (), this .keyManager .getKey (masterKeyId ));
208-
209- final Buffer b2 = Buffer .buffer (decryptedPayload );
210-
211- final long expiresMillis = b2 .getLong (0 );
212- final int siteKeyId = b2 .getInt (8 );
213-
214- final byte [] decryptedSitePayload = AesCbc .decrypt (b2 .slice (12 , b2 .length ()).getBytes (), this .keyManager .getKey (siteKeyId ));
215-
216- final Buffer b3 = Buffer .buffer (decryptedSitePayload );
217-
218- final int siteId = b3 .getInt (0 );
219- final int length = b3 .getInt (4 );
220-
221- final byte [] advertisingId = EncodingUtils .fromBase64 (b3 .slice (8 , 8 + length ).getBytes ());
222-
223- final int privacyBits = b3 .getInt (8 + length );
224- final long establishedMillis = b3 .getLong (8 + length + 4 );
225-
226- return new AdvertisingToken (
227- TokenVersion .V2 ,
228- Instant .ofEpochMilli (establishedMillis ),
229- Instant .ofEpochMilli (expiresMillis ),
230- new OperatorIdentity (0 , OperatorType .Service , 0 , masterKeyId ),
231- new PublisherIdentity (siteId , siteKeyId , 0 ),
232- new UserIdentity (IdentityScope .UID2 , IdentityType .Email , advertisingId , privacyBits , Instant .ofEpochMilli (establishedMillis ), null ),
233- siteKeyId
234- );
235-
236- } catch (Exception e ) {
237- throw new RuntimeException ("Couldn't decode advertisingTokenV2" , e );
238- }
154+ return decodeAdvertisingTokenV3orV4 (b , bytes , TokenVersion .V4 );
239155 }
240156
241157 public AdvertisingToken decodeAdvertisingTokenV3orV4 (Buffer b , byte [] bytes , TokenVersion tokenVersion ) {
@@ -284,32 +200,14 @@ private void recordRefreshTokenVersionCount(String siteId, TokenVersion tokenVer
284200 public byte [] encode (RefreshToken t , Instant asOf ) {
285201 final KeysetKey serviceKey = this .keyManager .getRefreshKey (asOf );
286202
287- switch (t .version ) {
288- case V2 :
289- recordRefreshTokenVersionCount (String .valueOf (t .publisherIdentity .siteId ), TokenVersion .V2 );
290- return encodeV2 (t , serviceKey );
291- case V3 :
292- recordRefreshTokenVersionCount (String .valueOf (t .publisherIdentity .siteId ), TokenVersion .V3 );
293- return encodeV3 (t , serviceKey );
294- default :
295- throw new ClientInputValidationException ("RefreshToken version " + t .version + " not supported" );
203+ if (t .version != TokenVersion .V4 ) {
204+ throw new ClientInputValidationException ("RefreshToken version " + t .version + " not supported" );
296205 }
206+ recordRefreshTokenVersionCount (String .valueOf (t .publisherIdentity .siteId ), TokenVersion .V4 );
207+ return encodeV4 (t , serviceKey );
297208 }
298209
299- public byte [] encodeV2 (RefreshToken t , KeysetKey serviceKey ) {
300- final Buffer b = Buffer .buffer ();
301- b .appendByte ((byte ) t .version .rawVersion );
302- b .appendLong (t .createdAt .toEpochMilli ());
303- b .appendLong (t .expiresAt .toEpochMilli ()); // should not be used
304- // give an extra minute for clients which are trying to refresh tokens close to or at the refresh expiry timestamp
305- b .appendLong (t .expiresAt .plusSeconds (60 ).toEpochMilli ());
306- b .appendInt (serviceKey .getId ());
307- final byte [] encryptedIdentity = encryptIdentityV2 (t .publisherIdentity , t .userIdentity , serviceKey );
308- b .appendBytes (encryptedIdentity );
309- return b .getBytes ();
310- }
311-
312- public byte [] encodeV3 (RefreshToken t , KeysetKey serviceKey ) {
210+ private byte [] encodeV4 (RefreshToken t , KeysetKey serviceKey ) {
313211 final Buffer refreshPayload = Buffer .buffer (90 );
314212 refreshPayload .appendLong (t .expiresAt .toEpochMilli ());
315213 refreshPayload .appendLong (t .createdAt .toEpochMilli ());
@@ -322,19 +220,13 @@ public byte[] encodeV3(RefreshToken t, KeysetKey serviceKey) {
322220
323221 final Buffer b = Buffer .buffer (124 );
324222 b .appendByte (encodeIdentityTypeV3 (t .userIdentity ));
325- b .appendByte ((byte ) t . version .rawVersion );
223+ b .appendByte ((byte ) TokenVersion . V4 .rawVersion );
326224 b .appendInt (serviceKey .getId ());
327225 b .appendBytes (AesGcm .encrypt (refreshPayload .getBytes (), serviceKey ).getPayload ());
328226
329227 return b .getBytes ();
330228 }
331229
332- private void encodeSiteIdentityV2 (Buffer b , PublisherIdentity publisherIdentity , UserIdentity userIdentity , KeysetKey siteEncryptionKey ) {
333- b .appendInt (siteEncryptionKey .getId ());
334- final byte [] encryptedIdentity = encryptIdentityV2 (publisherIdentity , userIdentity , siteEncryptionKey );
335- b .appendBytes (encryptedIdentity );
336- }
337-
338230 public static String bytesToBase64Token (byte [] advertisingTokenBytes , TokenVersion tokenVersion ) {
339231 return (tokenVersion == TokenVersion .V4 ) ?
340232 Uid2Base64UrlCoder .encode (advertisingTokenBytes ) : EncodingUtils .toBase64String (advertisingTokenBytes );
@@ -355,21 +247,6 @@ public IdentityTokens encode(AdvertisingToken advertisingToken, RefreshToken ref
355247 );
356248 }
357249
358- private byte [] encryptIdentityV2 (PublisherIdentity publisherIdentity , UserIdentity identity , KeysetKey key ) {
359- Buffer b = Buffer .buffer ();
360- try {
361- b .appendInt (publisherIdentity .siteId );
362- final byte [] identityBytes = EncodingUtils .toBase64 (identity .id );
363- b .appendInt (identityBytes .length );
364- b .appendBytes (identityBytes );
365- b .appendInt (identity .privacyBits );
366- b .appendLong (identity .establishedAt .toEpochMilli ());
367- return AesCbc .encrypt (b .getBytes (), key ).getPayload ();
368- } catch (Exception e ) {
369- throw new RuntimeException ("Could not turn Identity into UTF-8" , e );
370- }
371- }
372-
373250 private static byte encodeIdentityTypeV3 (UserIdentity userIdentity ) {
374251 return (byte ) (TokenUtils .encodeIdentityScope (userIdentity .identityScope ) | (userIdentity .identityType .getValue () << 2 ) | 3 );
375252 // "| 3" is used so that the 2nd char matches the version when V3 or higher. Eg "3" for V3 and "4" for V4
0 commit comments