diff --git a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java index 630918e19..55306a107 100644 --- a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java +++ b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java @@ -4,7 +4,6 @@ import com.uid2.operator.util.Tuple; import com.uid2.operator.vertx.ClientInputValidationException; import com.uid2.shared.Const.Data; -import com.uid2.shared.encryption.AesCbc; import com.uid2.shared.encryption.AesGcm; import com.uid2.shared.encryption.Uid2Base64UrlCoder; import com.uid2.shared.model.KeysetKey; @@ -13,11 +12,14 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Metrics; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; import java.util.HashMap; import java.util.Map; +import io.vertx.core.json.JsonObject; + public class EncryptedTokenEncoder implements ITokenEncoder { private final KeyManager keyManager; private final Map, Counter> siteKeysetStatusMetrics = new HashMap<>(); @@ -29,27 +31,7 @@ public EncryptedTokenEncoder(KeyManager keyManager) { public byte[] encode(AdvertisingToken t, Instant asOf) { final KeysetKey masterKey = this.keyManager.getMasterKey(asOf); final KeysetKey siteEncryptionKey = this.keyManager.getActiveKeyBySiteIdWithFallback(t.publisherIdentity.siteId, Data.AdvertisingTokenSiteId, asOf, siteKeysetStatusMetrics); - - return t.version == TokenVersion.V2 - ? encodeV2(t, masterKey, siteEncryptionKey) - : encodeV3(t, masterKey, siteEncryptionKey); //TokenVersion.V4 also calls encodeV3() since the byte array is identical between V3 and V4 - } - - private byte[] encodeV2(AdvertisingToken t, KeysetKey masterKey, KeysetKey siteKey) { - final Buffer b = Buffer.buffer(); - - b.appendByte((byte) t.version.rawVersion); - b.appendInt(masterKey.getId()); - - Buffer b2 = Buffer.buffer(); - b2.appendLong(t.expiresAt.toEpochMilli()); - encodeSiteIdentityV2(b2, t.publisherIdentity, t.userIdentity, siteKey); - - final byte[] encryptedId = AesCbc.encrypt(b2.getBytes(), masterKey).getPayload(); - - b.appendBytes(encryptedId); - - return b.getBytes(); + return encodeV3(t, masterKey, siteEncryptionKey); // TokenVersion.V4 calls encodeV3() since the byte array is identical between V3 and V4 } private byte[] encodeV3(AdvertisingToken t, KeysetKey masterKey, KeysetKey siteKey) { @@ -86,52 +68,40 @@ public RefreshToken decodeRefreshToken(String s) { throw new ClientInputValidationException("Invalid refresh token"); } final Buffer b = Buffer.buffer(bytes); - if (b.getByte(1) == TokenVersion.V3.rawVersion) { - return decodeRefreshTokenV3(b, bytes); - } else if (b.getByte(0) == TokenVersion.V2.rawVersion) { - return decodeRefreshTokenV2(b); + if (bytes.length >= 6 && (b.getByte(1) & 0xff) == TokenVersion.V4.rawVersion) { + return decodeRefreshTokenV4(b, bytes); } } throw new ClientInputValidationException("Invalid refresh token version"); } - private RefreshToken decodeRefreshTokenV2(Buffer b) { - final Instant createdAt = Instant.ofEpochMilli(b.getLong(1)); - //final Instant expiresAt = Instant.ofEpochMilli(b.getLong(9)); - final Instant validTill = Instant.ofEpochMilli(b.getLong(17)); - final int keyId = b.getInt(25); - - final KeysetKey key = this.keyManager.getKey(keyId); - + /** + * Unwraps the V2 API refresh token envelope (scope + keyId + encrypted JSON) and returns + * the inner refresh token string. Used when the response body contains refresh_response_key + * and refresh_token is this wrapped form rather than the raw V4 token. + */ + public String unwrapV2RefreshEnvelope(String wrappedBase64) { + byte[] bytes = EncodingUtils.fromBase64(wrappedBase64); + if (bytes.length < 5) { + throw new ClientInputValidationException("Invalid V2 refresh envelope"); + } + Buffer b = Buffer.buffer(bytes); + int keyId = b.getInt(1); + KeysetKey key = this.keyManager.getKey(keyId); if (key == null) { throw new ClientInputValidationException("Failed to fetch key with id: " + keyId); } - - final byte[] decryptedPayload = AesCbc.decrypt(b.slice(29, b.length()).getBytes(), key); - - final Buffer b2 = Buffer.buffer(decryptedPayload); - - final int siteId = b2.getInt(0); - final int length = b2.getInt(4); - final byte[] identity; - try { - identity = EncodingUtils.fromBase64(b2.slice(8, 8 + length).getBytes()); - } catch (Exception e) { - throw new ClientInputValidationException("Failed to decode refreshTokenV2: Identity segment is not valid base64.", e); + byte[] decrypted = AesGcm.decrypt(bytes, 5, key); + JsonObject json = new JsonObject(new String(decrypted, StandardCharsets.UTF_8)); + String innerToken = json.getString("refresh_token"); + if (innerToken == null) { + throw new ClientInputValidationException("V2 refresh envelope missing refresh_token"); } - - final int privacyBits = b2.getInt(8 + length); - final long establishedMillis = b2.getLong(8 + length + 4); - - return new RefreshToken( - TokenVersion.V2, createdAt, validTill, - new OperatorIdentity(0, OperatorType.Service, 0, 0), - new PublisherIdentity(siteId, 0, 0), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, identity, privacyBits, Instant.ofEpochMilli(establishedMillis), null)); + return innerToken; } - private RefreshToken decodeRefreshTokenV3(Buffer b, byte[] bytes) { + private RefreshToken decodeRefreshTokenV4(Buffer b, byte[] bytes) { final int keyId = b.getInt(2); final KeysetKey key = this.keyManager.getKey(keyId); @@ -153,20 +123,19 @@ private RefreshToken decodeRefreshTokenV3(Buffer b, byte[] bytes) { final byte[] id = b2.getBytes(58, 90); if (identityScope != decodeIdentityScopeV3(b.getByte(0))) { - throw new ClientInputValidationException("Failed to decode refreshTokenV3: Identity scope mismatch"); + throw new ClientInputValidationException("Failed to decode refreshTokenV4: Identity scope mismatch"); } if (identityType != decodeIdentityTypeV3(b.getByte(0))) { - throw new ClientInputValidationException("Failed to decode refreshTokenV3: Identity type mismatch"); + throw new ClientInputValidationException("Failed to decode refreshTokenV4: Identity type mismatch"); } return new RefreshToken( - TokenVersion.V3, createdAt, expiresAt, operatorIdentity, publisherIdentity, + TokenVersion.V4, createdAt, expiresAt, operatorIdentity, publisherIdentity, new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, null)); } @Override public AdvertisingToken decodeAdvertisingToken(String base64AdvertisingToken) { - //Logic and code copied from: https://github.com/IABTechLab/uid2-client-java/blob/0220ef43c1661ecf3b8f4ed2db524e2db31c06b5/src/main/java/com/uid2/client/Uid2Encryption.java#L37 if (base64AdvertisingToken.length() < 4) { throw new ClientInputValidationException("Advertising token is too short"); } @@ -175,67 +144,14 @@ public AdvertisingToken decodeAdvertisingToken(String base64AdvertisingToken) { boolean isBase64UrlEncoding = (headerStr.indexOf('-') != -1 || headerStr.indexOf('_') != -1); byte[] headerBytes = isBase64UrlEncoding ? Uid2Base64UrlCoder.decode(headerStr) : Base64.getDecoder().decode(headerStr); - if (headerBytes[0] == TokenVersion.V2.rawVersion) { - final byte[] bytes = EncodingUtils.fromBase64(base64AdvertisingToken); - final Buffer b = Buffer.buffer(bytes); - return decodeAdvertisingTokenV2(b); - } - - //Java's byte is signed, so we convert to unsigned before checking the enum int unsignedByte = ((int) headerBytes[1]) & 0xff; - - byte[] bytes; - TokenVersion tokenVersion; - if (unsignedByte == TokenVersion.V3.rawVersion) { - bytes = EncodingUtils.fromBase64(base64AdvertisingToken); - tokenVersion = TokenVersion.V3; - } else if (unsignedByte == TokenVersion.V4.rawVersion) { - bytes = Uid2Base64UrlCoder.decode(base64AdvertisingToken); //same as V3 but use Base64URL encoding - tokenVersion = TokenVersion.V4; - } else { - throw new ClientInputValidationException("Invalid advertising token version"); + if (unsignedByte != TokenVersion.V4.rawVersion) { + throw new ClientInputValidationException("V2/V3 advertising token no longer supported"); } + final byte[] bytes = Uid2Base64UrlCoder.decode(base64AdvertisingToken); final Buffer b = Buffer.buffer(bytes); - return decodeAdvertisingTokenV3orV4(b, bytes, tokenVersion); - } - - public AdvertisingToken decodeAdvertisingTokenV2(Buffer b) { - try { - final int masterKeyId = b.getInt(1); - - final byte[] decryptedPayload = AesCbc.decrypt(b.slice(5, b.length()).getBytes(), this.keyManager.getKey(masterKeyId)); - - final Buffer b2 = Buffer.buffer(decryptedPayload); - - final long expiresMillis = b2.getLong(0); - final int siteKeyId = b2.getInt(8); - - final byte[] decryptedSitePayload = AesCbc.decrypt(b2.slice(12, b2.length()).getBytes(), this.keyManager.getKey(siteKeyId)); - - final Buffer b3 = Buffer.buffer(decryptedSitePayload); - - final int siteId = b3.getInt(0); - final int length = b3.getInt(4); - - final byte[] advertisingId = EncodingUtils.fromBase64(b3.slice(8, 8 + length).getBytes()); - - final int privacyBits = b3.getInt(8 + length); - final long establishedMillis = b3.getLong(8 + length + 4); - - return new AdvertisingToken( - TokenVersion.V2, - Instant.ofEpochMilli(establishedMillis), - Instant.ofEpochMilli(expiresMillis), - new OperatorIdentity(0, OperatorType.Service, 0, masterKeyId), - new PublisherIdentity(siteId, siteKeyId, 0), - new UserIdentity(IdentityScope.UID2, IdentityType.Email, advertisingId, privacyBits, Instant.ofEpochMilli(establishedMillis), null), - siteKeyId - ); - - } catch (Exception e) { - throw new RuntimeException("Couldn't decode advertisingTokenV2", e); - } + return decodeAdvertisingTokenV3orV4(b, bytes, TokenVersion.V4); } public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, TokenVersion tokenVersion) { @@ -284,32 +200,14 @@ private void recordRefreshTokenVersionCount(String siteId, TokenVersion tokenVer public byte[] encode(RefreshToken t, Instant asOf) { final KeysetKey serviceKey = this.keyManager.getRefreshKey(asOf); - switch (t.version) { - case V2: - recordRefreshTokenVersionCount(String.valueOf(t.publisherIdentity.siteId), TokenVersion.V2); - return encodeV2(t, serviceKey); - case V3: - recordRefreshTokenVersionCount(String.valueOf(t.publisherIdentity.siteId), TokenVersion.V3); - return encodeV3(t, serviceKey); - default: - throw new ClientInputValidationException("RefreshToken version " + t.version + " not supported"); + if (t.version != TokenVersion.V4) { + throw new ClientInputValidationException("RefreshToken version " + t.version + " not supported"); } + recordRefreshTokenVersionCount(String.valueOf(t.publisherIdentity.siteId), TokenVersion.V4); + return encodeV4(t, serviceKey); } - public byte[] encodeV2(RefreshToken t, KeysetKey serviceKey) { - final Buffer b = Buffer.buffer(); - b.appendByte((byte) t.version.rawVersion); - b.appendLong(t.createdAt.toEpochMilli()); - b.appendLong(t.expiresAt.toEpochMilli()); // should not be used - // give an extra minute for clients which are trying to refresh tokens close to or at the refresh expiry timestamp - b.appendLong(t.expiresAt.plusSeconds(60).toEpochMilli()); - b.appendInt(serviceKey.getId()); - final byte[] encryptedIdentity = encryptIdentityV2(t.publisherIdentity, t.userIdentity, serviceKey); - b.appendBytes(encryptedIdentity); - return b.getBytes(); - } - - public byte[] encodeV3(RefreshToken t, KeysetKey serviceKey) { + private byte[] encodeV4(RefreshToken t, KeysetKey serviceKey) { final Buffer refreshPayload = Buffer.buffer(90); refreshPayload.appendLong(t.expiresAt.toEpochMilli()); refreshPayload.appendLong(t.createdAt.toEpochMilli()); @@ -322,19 +220,13 @@ public byte[] encodeV3(RefreshToken t, KeysetKey serviceKey) { final Buffer b = Buffer.buffer(124); b.appendByte(encodeIdentityTypeV3(t.userIdentity)); - b.appendByte((byte) t.version.rawVersion); + b.appendByte((byte) TokenVersion.V4.rawVersion); b.appendInt(serviceKey.getId()); b.appendBytes(AesGcm.encrypt(refreshPayload.getBytes(), serviceKey).getPayload()); return b.getBytes(); } - private void encodeSiteIdentityV2(Buffer b, PublisherIdentity publisherIdentity, UserIdentity userIdentity, KeysetKey siteEncryptionKey) { - b.appendInt(siteEncryptionKey.getId()); - final byte[] encryptedIdentity = encryptIdentityV2(publisherIdentity, userIdentity, siteEncryptionKey); - b.appendBytes(encryptedIdentity); - } - public static String bytesToBase64Token(byte[] advertisingTokenBytes, TokenVersion tokenVersion) { return (tokenVersion == TokenVersion.V4) ? Uid2Base64UrlCoder.encode(advertisingTokenBytes) : EncodingUtils.toBase64String(advertisingTokenBytes); @@ -355,21 +247,6 @@ public IdentityTokens encode(AdvertisingToken advertisingToken, RefreshToken ref ); } - private byte[] encryptIdentityV2(PublisherIdentity publisherIdentity, UserIdentity identity, KeysetKey key) { - Buffer b = Buffer.buffer(); - try { - b.appendInt(publisherIdentity.siteId); - final byte[] identityBytes = EncodingUtils.toBase64(identity.id); - b.appendInt(identityBytes.length); - b.appendBytes(identityBytes); - b.appendInt(identity.privacyBits); - b.appendLong(identity.establishedAt.toEpochMilli()); - return AesCbc.encrypt(b.getBytes(), key).getPayload(); - } catch (Exception e) { - throw new RuntimeException("Could not turn Identity into UTF-8", e); - } - } - private static byte encodeIdentityTypeV3(UserIdentity userIdentity) { return (byte) (TokenUtils.encodeIdentityScope(userIdentity.identityScope) | (userIdentity.identityType.getValue() << 2) | 3); // "| 3" is used so that the 2nd char matches the version when V3 or higher. Eg "3" for V3 and "4" for V4 diff --git a/src/main/java/com/uid2/operator/service/UIDOperatorService.java b/src/main/java/com/uid2/operator/service/UIDOperatorService.java index 9662dc59d..83d99c636 100644 --- a/src/main/java/com/uid2/operator/service/UIDOperatorService.java +++ b/src/main/java/com/uid2/operator/service/UIDOperatorService.java @@ -85,7 +85,7 @@ public UIDOperatorService(IOptOutStore optOutStore, ISaltProvider saltProvider, this.operatorIdentity = new OperatorIdentity(0, OperatorType.Service, 0, 0); - this.refreshTokenVersion = TokenVersion.V3; + this.refreshTokenVersion = TokenVersion.V4; this.rawUidV3Enabled = identityV3Enabled; registerAdvertisingIdVersionCounter(IdentityVersion.V2); diff --git a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java index 503ac9ace..de0868005 100644 --- a/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java +++ b/src/main/java/com/uid2/operator/vertx/UIDOperatorVerticle.java @@ -1457,13 +1457,14 @@ private void recordOptOutStatusEndpointStats(RoutingContext rc, int inputCount, } public TokenVersion getRefreshTokenVersion(String s) { - if (s != null && !s.isEmpty()) { - final byte[] bytes = EncodingUtils.fromBase64(s); - final Buffer b = Buffer.buffer(bytes); - if (b.getByte(1) == TokenVersion.V3.rawVersion) { - return TokenVersion.V3; - } else if (b.getByte(0) == TokenVersion.V2.rawVersion) { - return TokenVersion.V2; + if (s != null && !s.isEmpty() && s.length() >= 4) { + try { + final byte[] bytes = EncodingUtils.fromBase64(s); + final Buffer b = Buffer.buffer(bytes); + if (bytes.length >= 6 && (b.getByte(1) & 0xff) == TokenVersion.V4.rawVersion) { + return TokenVersion.V4; + } + } catch (IllegalArgumentException ignored) { } } return null; diff --git a/src/test/java/com/uid2/operator/TokenEncodingTest.java b/src/test/java/com/uid2/operator/TokenEncodingTest.java index e7816776d..ad79a0362 100644 --- a/src/test/java/com/uid2/operator/TokenEncodingTest.java +++ b/src/test/java/com/uid2/operator/TokenEncodingTest.java @@ -14,7 +14,6 @@ import io.micrometer.core.instrument.Metrics; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; -import org.junit.Assert; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; @@ -45,7 +44,7 @@ public TokenEncodingTest() throws Exception { } @ParameterizedTest - @EnumSource(value = TokenVersion.class, names = {"V3", "V4"}) + @EnumSource(value = TokenVersion.class, names = {"V4"}) void testRefreshTokenEncoding(TokenVersion tokenVersion) { final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager); final Instant now = EncodingUtils.NowUTCMillis(); @@ -60,24 +59,19 @@ void testRefreshTokenEncoding(TokenVersion tokenVersion) { new UserIdentity(IdentityScope.UID2, IdentityType.Email, firstLevelHash, 121, now, now.minusSeconds(122)) ); - if (tokenVersion == TokenVersion.V4) { - Assert.assertThrows(Exception.class, () -> encoder.encode(token, now)); - return; //V4 not supported for RefreshTokens - } final byte[] encodedBytes = encoder.encode(token, now); final RefreshToken decoded = encoder.decodeRefreshToken(EncodingUtils.toBase64String(encodedBytes)); assertEquals(tokenVersion, decoded.version); assertEquals(token.createdAt, decoded.createdAt); - int addSeconds = (tokenVersion == TokenVersion.V2) ? 60 : 0; //todo: why is there a 60 second buffer in encodeV2() but not in encodeV3()? - assertEquals(token.expiresAt.plusSeconds(addSeconds), decoded.expiresAt); + assertEquals(token.expiresAt, decoded.expiresAt); assertTrue(token.userIdentity.matches(decoded.userIdentity)); assertEquals(token.userIdentity.privacyBits, decoded.userIdentity.privacyBits); assertEquals(token.userIdentity.establishedAt, decoded.userIdentity.establishedAt); assertEquals(token.publisherIdentity.siteId, decoded.publisherIdentity.siteId); Buffer b = Buffer.buffer(encodedBytes); - int keyId = b.getInt(tokenVersion == TokenVersion.V2 ? 25 : 2); + int keyId = b.getInt(2); assertEquals(Data.RefreshKeySiteId, keyManager.getSiteIdFromKeyId(keyId)); assertNotNull(Metrics.globalRegistry @@ -87,13 +81,8 @@ void testRefreshTokenEncoding(TokenVersion tokenVersion) { @ParameterizedTest @CsvSource({ - "false, V4", //same as current UID2 prod (as at 2024-12-10) - "true, V4", //same as current EUID prod (as at 2024-12-10) - //the following combinations aren't used in any UID2/EUID environments but just testing them regardless - "false, V3", - "true, V3", - "false, V2", - "true, V2" + "false, V4", + "true, V4" }) void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVersion) { final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager); @@ -122,7 +111,7 @@ void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVers assertEquals(token.publisherIdentity.siteId, decoded.publisherIdentity.siteId); Buffer b = Buffer.buffer(encodedBytes); - int keyId = b.getInt(adTokenVersion == TokenVersion.V2 ? 1 : 2); //TODO - extract master key from token should be a helper function + int keyId = b.getInt(2); assertEquals(Data.MasterKeySiteId, keyManager.getSiteIdFromKeyId(keyId)); } } diff --git a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java index a91bc57bb..f686f803c 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -323,7 +323,7 @@ private void sendTokenGenerate(Vertx vertx, JsonObject v2PostPayload, int expect JsonObject respJson = new JsonObject(new String(decrypted, 16, decrypted.length - 16, StandardCharsets.UTF_8)); - decodeV2RefreshToken(respJson); + ensureDecryptedRefreshTokenInBody(respJson); handler.handle(respJson); } else { @@ -362,7 +362,7 @@ private void sendTokenRefresh(Vertx vertx, VertxTestContext testContext, String JsonObject respJson = new JsonObject(new String(decrypted, StandardCharsets.UTF_8)); if (respJson.getString("status").equals("success")) - decodeV2RefreshToken(respJson); + ensureDecryptedRefreshTokenInBody(respJson); handler.handle(respJson); } else { @@ -371,23 +371,37 @@ private void sendTokenRefresh(Vertx vertx, VertxTestContext testContext, String }))); } - private String decodeV2RefreshToken(JsonObject respJson) { + /** For V4, refresh_token in body is the token; ensure decrypted_refresh_token is set for tests that read it. + * For V2 API responses, refresh_token is a wrapped envelope; unwrap it so decrypted_refresh_token is the + * raw V4 token that decodeRefreshToken expects. We unwrap when the token is not already V4 (byte 1 != 128). */ + private void ensureDecryptedRefreshTokenInBody(JsonObject respJson) { if (respJson.containsKey("body")) { JsonObject bodyJson = respJson.getJsonObject("body"); + if (bodyJson.getString("refresh_token") != null && bodyJson.getString("decrypted_refresh_token") == null) { + String refreshToken = bodyJson.getString("refresh_token"); + String tokenForDecode = isV2WrappedRefreshToken(refreshToken) + ? encoder.unwrapV2RefreshEnvelope(refreshToken) + : refreshToken; + bodyJson.put("decrypted_refresh_token", tokenForDecode); + } + } + } - byte[] tokenBytes = Utils.decodeBase64String(bodyJson.getString("refresh_token")); - KeysetKey refreshKey = keysetKeyStore.getSnapshot().getKey(Buffer.buffer(tokenBytes).getInt(1)); - - byte[] decrypted = AesGcm.decrypt(tokenBytes, 5, refreshKey); - JsonObject tokenKeyJson = new JsonObject(new String(decrypted)); - - String refreshToken = tokenKeyJson.getString("refresh_token"); - bodyJson.put("decrypted_refresh_token", refreshToken); - - return refreshToken; + /** True if the base64-decoded token looks like the V2 envelope (byte 1 is not V4 version 128). */ + private static boolean isV2WrappedRefreshToken(String refreshTokenBase64) { + try { + byte[] bytes = Utils.decodeBase64String(refreshTokenBase64); + return bytes.length >= 6 && (bytes[1] & 0xff) != TokenVersion.V4.rawVersion; + } catch (Exception e) { + return false; } + } - return null; + private String getRefreshTokenStringFromResponse(JsonObject response) { + JsonObject body = response.getJsonObject("body"); + if (body == null) return null; + String s = body.getString("decrypted_refresh_token"); + return s != null ? s : body.getString("refresh_token"); } private JsonObject tryParseResponse(HttpResponse resp) { @@ -916,28 +930,17 @@ private AdvertisingToken validateAndGetToken(EncryptedTokenEncoder encoder, Json } public static void validateAdvertisingToken(String advertisingTokenString, TokenVersion tokenVersion, IdentityScope identityScope, IdentityType identityType) { - if (tokenVersion == TokenVersion.V2) { - assertEquals("Ag", advertisingTokenString.substring(0, 2)); + // V4 only + String firstChar = advertisingTokenString.substring(0, 1); + if (identityScope == IdentityScope.UID2) { + assertEquals(identityType == IdentityType.Email ? "A" : "B", firstChar); } else { - String firstChar = advertisingTokenString.substring(0, 1); - if (identityScope == IdentityScope.UID2) { - assertEquals(identityType == IdentityType.Email ? "A" : "B", firstChar); - } else { - assertEquals(identityType == IdentityType.Email ? "E" : "F", firstChar); - } - - String secondChar = advertisingTokenString.substring(1, 2); - if (tokenVersion == TokenVersion.V3) { - assertEquals("3", secondChar); - } else { - assertEquals("4", secondChar); - - //No URL-unfriendly characters allowed: - assertEquals(-1, advertisingTokenString.indexOf('=')); - assertEquals(-1, advertisingTokenString.indexOf('+')); - assertEquals(-1, advertisingTokenString.indexOf('/')); - } + assertEquals(identityType == IdentityType.Email ? "E" : "F", firstChar); } + assertEquals("4", advertisingTokenString.substring(1, 2)); + assertEquals(-1, advertisingTokenString.indexOf('=')); + assertEquals(-1, advertisingTokenString.indexOf('+')); + assertEquals(-1, advertisingTokenString.indexOf('/')); } RefreshToken decodeRefreshToken(EncryptedTokenEncoder encoder, String refreshTokenString, IdentityType identityType) { @@ -2991,6 +2994,7 @@ private void sendCstg(Vertx vertx, String endpoint, String httpOriginHeader, Jso if (result.statusCode() == 200) { byte[] decrypted = decrypt(Utils.decodeBase64String(result.bodyAsString()), 0, secretKey.getEncoded()); JsonObject respJson = new JsonObject(new String(decrypted, 0, decrypted.length - 0, StandardCharsets.UTF_8)); + ensureDecryptedRefreshTokenInBody(respJson); handler.handle(respJson); } else { //errors is in plain text handler.handle(tryParseResponse(result)); @@ -3841,7 +3845,7 @@ void cstgUserOptsOutAfterTokenGenerate( final JsonObject genBody = response.getJsonObject("body"); final AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); - final RefreshToken refreshToken = decodeRefreshToken(encoder, decodeV2RefreshToken(response), identityType); + final RefreshToken refreshToken = decodeRefreshToken(encoder, getRefreshTokenStringFromResponse(response), identityType); assertAreClientSideGeneratedTokens(advertisingToken, refreshToken, clientSideTokenGenerateSiteId, identityType, id, salt, false, useV4Uid, false); @@ -3923,7 +3927,7 @@ void cstgSuccessForBothOptedAndNonOptedOutTest( JsonObject genBody = respJson.getJsonObject("body"); assertNotNull(genBody); - decodeV2RefreshToken(respJson); + ensureDecryptedRefreshTokenInBody(respJson); AdvertisingToken advertisingToken = validateAndGetToken(encoder, genBody, identityType); assertArrayEquals(getAdvertisingIdFromIdentity(identityType, id, firstLevelSalt, salt, useV4Uid, false), advertisingToken.userIdentity.id); @@ -4324,25 +4328,14 @@ void tokenGenerateRotatingKeysets_GENERATOR(String testRun, Vertx vertx, VertxTe AdvertisingToken advertisingToken = validateAndGetToken(encoder, body, IdentityType.Email); assertEquals(clientSiteId, advertisingToken.publisherIdentity.siteId); - //Uses a key from default keyset - int clientKeyId; - if (advertisingToken.version == TokenVersion.V3 || advertisingToken.version == TokenVersion.V4) { - String advertisingTokenString = body.getString("advertising_token"); - byte[] bytes = null; - if (advertisingToken.version == TokenVersion.V3) { - bytes = EncodingUtils.fromBase64(advertisingTokenString); - } else if (advertisingToken.version == TokenVersion.V4) { - bytes = Uid2Base64UrlCoder.decode(advertisingTokenString); //same as V3 but use Base64URL encoding - } - final Buffer b = Buffer.buffer(bytes); - final int masterKeyId = b.getInt(2); - - final byte[] masterPayloadBytes = AesGcm.decrypt(bytes, 6, keysetKeyStore.getSnapshot().getKey(masterKeyId)); - final Buffer masterPayload = Buffer.buffer(masterPayloadBytes); - clientKeyId = masterPayload.getInt(29); - } else { - clientKeyId = advertisingToken.publisherIdentity.clientKeyId; - } + // V4 only: uses a key from default keyset + String advertisingTokenString = body.getString("advertising_token"); + byte[] bytes = Uid2Base64UrlCoder.decode(advertisingTokenString); + final Buffer b = Buffer.buffer(bytes); + final int masterKeyId = b.getInt(2); + final byte[] masterPayloadBytes = AesGcm.decrypt(bytes, 6, keysetKeyStore.getSnapshot().getKey(masterKeyId)); + final Buffer masterPayload = Buffer.buffer(masterPayloadBytes); + int clientKeyId = masterPayload.getInt(29); switch (testRun) { case "MultiKeysets": assertEquals(1007, clientKeyId); // should encrypt with active key in default keyset