Skip to content

Commit b09156c

Browse files
committed
Remove V2 and V3 advertising token
1 parent f0e98b6 commit b09156c

5 files changed

Lines changed: 102 additions & 242 deletions

File tree

src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java

Lines changed: 39 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.uid2.operator.util.Tuple;
55
import com.uid2.operator.vertx.ClientInputValidationException;
66
import com.uid2.shared.Const.Data;
7-
import com.uid2.shared.encryption.AesCbc;
87
import com.uid2.shared.encryption.AesGcm;
98
import com.uid2.shared.encryption.Uid2Base64UrlCoder;
109
import com.uid2.shared.model.KeysetKey;
@@ -13,11 +12,14 @@
1312
import io.micrometer.core.instrument.Counter;
1413
import io.micrometer.core.instrument.Metrics;
1514

15+
import java.nio.charset.StandardCharsets;
1616
import java.time.Instant;
1717
import java.util.Base64;
1818
import java.util.HashMap;
1919
import java.util.Map;
2020

21+
import io.vertx.core.json.JsonObject;
22+
2123
public 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

src/main/java/com/uid2/operator/service/UIDOperatorService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider,
8585

8686
this.operatorIdentity = new OperatorIdentity(0, OperatorType.Service, 0, 0);
8787

88-
this.refreshTokenVersion = TokenVersion.V3;
88+
this.refreshTokenVersion = TokenVersion.V4;
8989
this.rawUidV3Enabled = identityV3Enabled;
9090

9191
registerAdvertisingIdVersionCounter(IdentityVersion.V2);

src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,13 +1457,14 @@ private void recordOptOutStatusEndpointStats(RoutingContext rc, int inputCount,
14571457
}
14581458

14591459
public TokenVersion getRefreshTokenVersion(String s) {
1460-
if (s != null && !s.isEmpty()) {
1461-
final byte[] bytes = EncodingUtils.fromBase64(s);
1462-
final Buffer b = Buffer.buffer(bytes);
1463-
if (b.getByte(1) == TokenVersion.V3.rawVersion) {
1464-
return TokenVersion.V3;
1465-
} else if (b.getByte(0) == TokenVersion.V2.rawVersion) {
1466-
return TokenVersion.V2;
1460+
if (s != null && !s.isEmpty() && s.length() >= 4) {
1461+
try {
1462+
final byte[] bytes = EncodingUtils.fromBase64(s);
1463+
final Buffer b = Buffer.buffer(bytes);
1464+
if (bytes.length >= 6 && (b.getByte(1) & 0xff) == TokenVersion.V4.rawVersion) {
1465+
return TokenVersion.V4;
1466+
}
1467+
} catch (IllegalArgumentException ignored) {
14671468
}
14681469
}
14691470
return null;

0 commit comments

Comments
 (0)