diff --git a/src/main/java/com/uid2/operator/model/IdentityEnvironment.java b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java new file mode 100644 index 000000000..bf462912b --- /dev/null +++ b/src/main/java/com/uid2/operator/model/IdentityEnvironment.java @@ -0,0 +1,20 @@ +package com.uid2.operator.model; + +import com.uid2.operator.vertx.ClientInputValidationException; + +public enum IdentityEnvironment { + Test(0), Integ(1), Prod(2); + + public final int value; + + IdentityEnvironment(int value) { this.value = value; } + + public static IdentityEnvironment fromValue(int value) { + return switch (value) { + case 0 -> Test; + case 1 -> Integ; + case 2 -> Prod; + default -> throw new ClientInputValidationException("Invalid valid for IdentityEnvironment: " + value); + }; + } +} diff --git a/src/main/java/com/uid2/operator/model/IdentityVersion.java b/src/main/java/com/uid2/operator/model/IdentityVersion.java new file mode 100644 index 000000000..995071027 --- /dev/null +++ b/src/main/java/com/uid2/operator/model/IdentityVersion.java @@ -0,0 +1,19 @@ +package com.uid2.operator.model; + +import com.uid2.operator.vertx.ClientInputValidationException; + +public enum IdentityVersion { + V3(0), V4(1); + + public final int value; + + IdentityVersion(int value) { this.value = value; } + + public static IdentityVersion fromValue(int value) { + return switch (value) { + case 0 -> V3; + case 1 -> V4; + default -> throw new ClientInputValidationException("Invalid valid for IdentityVersion: " + value); + }; + } +} diff --git a/src/main/java/com/uid2/operator/service/TokenUtils.java b/src/main/java/com/uid2/operator/service/TokenUtils.java index 2cabc641b..bbf6f363d 100644 --- a/src/main/java/com/uid2/operator/service/TokenUtils.java +++ b/src/main/java/com/uid2/operator/service/TokenUtils.java @@ -1,10 +1,10 @@ package com.uid2.operator.service; +import com.uid2.operator.model.IdentityEnvironment; import com.uid2.operator.model.IdentityScope; import com.uid2.operator.model.IdentityType; - -import java.util.HashSet; -import java.util.Set; +import com.uid2.operator.model.IdentityVersion; +import com.uid2.shared.model.SaltEntry; public class TokenUtils { public static byte[] getIdentityHash(String identityString) { @@ -55,11 +55,18 @@ public static byte[] getAdvertisingIdV3FromIdentityHash(IdentityScope scope, Ide return getAdvertisingIdV3(scope, type, getFirstLevelHashFromIdentityHash(identityString, firstLevelSalt), rotatingSalt); } - public static byte encodeIdentityScope(IdentityScope identityScope) { - return (byte) (identityScope.value << 4); + public static byte[] getAdvertisingIdV4(IdentityScope scope, IdentityType type, IdentityEnvironment environment, byte[] firstLevelHash, SaltEntry.KeyMaterial encryptingKey, String rotatingSalt) throws Exception { + byte metadata = (byte) (encodeIdentityVersion(IdentityVersion.V4) | encodeIdentityScope(scope) | encodeIdentityType(type) | encodeIdentityEnvironment(environment)); + return V4TokenUtils.buildAdvertisingIdV4(metadata, firstLevelHash, encryptingKey.id(), encryptingKey.key(), rotatingSalt); } + public static byte encodeIdentityScope(IdentityScope identityScope) { return (byte) (identityScope.value << 4); } + public static byte encodeIdentityType(IdentityType identityType) { return (byte) (identityType.value << 2); } + + public static byte encodeIdentityVersion(IdentityVersion identityVersion) { return (byte) (identityVersion.value << 6); } + + public static byte encodeIdentityEnvironment(IdentityEnvironment identityEnvironment) { return (byte) (identityEnvironment.value); } } diff --git a/src/main/java/com/uid2/operator/service/V4TokenUtils.java b/src/main/java/com/uid2/operator/service/V4TokenUtils.java new file mode 100644 index 000000000..fc57e8806 --- /dev/null +++ b/src/main/java/com/uid2/operator/service/V4TokenUtils.java @@ -0,0 +1,67 @@ +package com.uid2.operator.service; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import io.vertx.core.buffer.Buffer; +import java.util.Arrays; + +public class V4TokenUtils { + public static byte[] generateIV(String salt, byte[] firstLevelHashLast16Bytes, byte metadata, int keyId) { + int iv_length = 12; + String iv_base = salt + .concat(Arrays.toString(firstLevelHashLast16Bytes)) + .concat(Byte.toString(metadata)) + .concat(String.valueOf(keyId)); + return Arrays.copyOfRange(EncodingUtils.getSha256Bytes(iv_base), 0, iv_length); + } + + private static byte[] padIV16Bytes(byte[] iv) { + // Pad the 12-byte IV to 16 bytes for AES-CTR (standard block size) + byte[] paddedIV = new byte[16]; + System.arraycopy(iv, 0, paddedIV, 0, 12); + // Remaining 4 bytes are already zero-initialized (counter starts at 0) + return paddedIV; + } + + private static byte[] encryptHash(String encryptionKey, byte[] hash, byte[] iv) throws Exception { + // Set up AES256-CTR cipher + Cipher aesCtr = Cipher.getInstance("AES/CTR/NoPadding"); + SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(), "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(padIV16Bytes(iv)); + + aesCtr.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); + return aesCtr.doFinal(hash); + } + + public static byte generateChecksum(byte[] data) { + // Simple XOR checksum of all bytes + byte checksum = 0; + for (byte b : data) { + checksum ^= b; + } + System.out.println("Checksum: 0x" + String.format("%02X", checksum)); + return checksum; + } + + public static byte[] buildAdvertisingIdV4(byte metadata, byte[] firstLevelHash, int keyId, String key, String salt) throws Exception { + byte[] hash16Bytes = Arrays.copyOfRange(firstLevelHash, 0, 16); + byte[] iv = V4TokenUtils.generateIV(salt, hash16Bytes, metadata, keyId); + byte[] encryptedFirstLevelHash = V4TokenUtils.encryptHash(key, hash16Bytes, iv); + + Buffer buffer = Buffer.buffer(); + buffer.appendByte(metadata); + buffer.appendBytes(new byte[] { + (byte) (keyId & 0xFF), // LSB + (byte) ((keyId >> 8) & 0xFF), // Middle + (byte) ((keyId >> 16) & 0xFF) // MSB + }); + buffer.appendBytes(iv); + buffer.appendBytes(encryptedFirstLevelHash); + + byte checksum = generateChecksum(buffer.getBytes()); + buffer.appendByte(checksum); + + return buffer.getBytes(); + } +}