diff --git a/pom.xml b/pom.xml index f1d08c85c..18f1a537d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-operator - 5.70.14 + 5.70.15-alpha-324-SNAPSHOT UTF-8 diff --git a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java index 630918e19..a2dac487e 100644 --- a/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java +++ b/src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java @@ -27,29 +27,12 @@ public EncryptedTokenEncoder(KeyManager keyManager) { } public byte[] encode(AdvertisingToken t, Instant asOf) { + if (t.version != TokenVersion.V4) { + throw new ClientInputValidationException("Only advertising token V4 is supported"); + } 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); } private byte[] encodeV3(AdvertisingToken t, KeysetKey masterKey, KeysetKey siteKey) { @@ -176,69 +159,25 @@ public AdvertisingToken decodeAdvertisingToken(String base64AdvertisingToken) { 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); + throw new ClientInputValidationException("Advertising token V2 is no longer supported"); } //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("Advertising token V3 is no longer supported"); + } + if (unsignedByte != TokenVersion.V4.rawVersion) { throw new ClientInputValidationException("Invalid advertising token version"); } + 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 decodeAdvertisingTokenV4(b, bytes); } - public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, TokenVersion tokenVersion) { + private AdvertisingToken decodeAdvertisingTokenV4(Buffer b, byte[] bytes) { final int masterKeyId = b.getInt(2); final byte[] masterPayloadBytes = AesGcm.decrypt(bytes, 6, this.keyManager.getKey(masterKeyId)); @@ -259,15 +198,15 @@ public AdvertisingToken decodeAdvertisingTokenV3orV4(Buffer b, byte[] bytes, Tok if (id.length > 32) { if (identityScope != decodeIdentityScopeV3(b.getByte(0))) { - throw new ClientInputValidationException("Failed decoding advertisingTokenV3: Identity scope mismatch"); + throw new ClientInputValidationException("Failed decoding advertising token: Identity scope mismatch"); } if (identityType != decodeIdentityTypeV3(b.getByte(0))) { - throw new ClientInputValidationException("Failed decoding advertisingTokenV3: Identity type mismatch"); + throw new ClientInputValidationException("Failed decoding advertising token: Identity type mismatch"); } } return new AdvertisingToken( - tokenVersion, createdAt, expiresAt, operatorIdentity, publisherIdentity, + TokenVersion.V4, createdAt, expiresAt, operatorIdentity, publisherIdentity, new UserIdentity(identityScope, identityType, id, privacyBits, establishedAt, refreshedAt), siteKeyId ); @@ -329,15 +268,11 @@ public byte[] encodeV3(RefreshToken t, KeysetKey serviceKey) { 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); + if (tokenVersion != TokenVersion.V4) { + throw new ClientInputValidationException("Only advertising token V4 is supported"); + } + return Uid2Base64UrlCoder.encode(advertisingTokenBytes); } @Override diff --git a/src/test/java/com/uid2/operator/TokenEncodingTest.java b/src/test/java/com/uid2/operator/TokenEncodingTest.java index e7816776d..7dd0ed6c7 100644 --- a/src/test/java/com/uid2/operator/TokenEncodingTest.java +++ b/src/test/java/com/uid2/operator/TokenEncodingTest.java @@ -16,8 +16,8 @@ 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; +import org.junit.jupiter.params.provider.ValueSource; import java.time.Instant; @@ -86,23 +86,15 @@ 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" - }) - void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVersion) { + @ValueSource(booleans = {false, true}) + void testAdvertisingTokenEncodings(boolean useRawUIDv3) { final EncryptedTokenEncoder encoder = new EncryptedTokenEncoder(this.keyManager); final Instant now = EncodingUtils.NowUTCMillis(); final byte[] rawUid = UIDOperatorVerticleTest.getRawUid(IdentityScope.UID2, IdentityType.Email, "test@example.com", useRawUIDv3); final AdvertisingToken token = new AdvertisingToken( - adTokenVersion, + TokenVersion.V4, now, now.plusSeconds(60), new OperatorIdentity(101, OperatorType.Service, 102, 103), @@ -111,9 +103,9 @@ void testAdvertisingTokenEncodings(boolean useRawUIDv3, TokenVersion adTokenVers ); final byte[] encodedBytes = encoder.encode(token, now); - final AdvertisingToken decoded = encoder.decodeAdvertisingToken(EncryptedTokenEncoder.bytesToBase64Token(encodedBytes, adTokenVersion)); + final AdvertisingToken decoded = encoder.decodeAdvertisingToken(EncryptedTokenEncoder.bytesToBase64Token(encodedBytes, TokenVersion.V4)); - assertEquals(adTokenVersion, decoded.version); + assertEquals(TokenVersion.V4, decoded.version); assertEquals(token.createdAt, decoded.createdAt); assertEquals(token.expiresAt, decoded.expiresAt); assertTrue(token.userIdentity.matches(decoded.userIdentity)); @@ -122,7 +114,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 7159adc9d..8b159b757 100644 --- a/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java +++ b/src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java @@ -917,28 +917,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)); + assertEquals(TokenVersion.V4, tokenVersion); + 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) { @@ -4392,24 +4381,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; - } + assertEquals(TokenVersion.V4, advertisingToken.version); + 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