Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.uid2</groupId>
<artifactId>uid2-operator</artifactId>
<version>5.70.14</version>
<version>5.70.15-alpha-324-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
101 changes: 18 additions & 83 deletions src/main/java/com/uid2/operator/service/EncryptedTokenEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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));
Expand All @@ -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
);
Expand Down Expand Up @@ -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
Expand Down
22 changes: 7 additions & 15 deletions src/test/java/com/uid2/operator/TokenEncodingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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, "[email protected]", useRawUIDv3);

final AdvertisingToken token = new AdvertisingToken(
adTokenVersion,
TokenVersion.V4,
now,
now.plusSeconds(60),
new OperatorIdentity(101, OperatorType.Service, 102, 103),
Expand All @@ -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));
Expand All @@ -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));
}
}
55 changes: 17 additions & 38 deletions src/test/java/com/uid2/operator/UIDOperatorVerticleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Loading