-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add CSTG tests #7
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,24 @@ | |
import app.common.HttpClient; | ||
import app.common.Mapper; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.google.gson.JsonArray; | ||
import com.google.gson.JsonObject; | ||
import com.uid2.client.*; | ||
import com.uid2.client.IdentityScope; | ||
import okhttp3.Request; | ||
import okhttp3.RequestBody; | ||
|
||
import javax.crypto.*; | ||
import javax.crypto.spec.GCMParameterSpec; | ||
import javax.crypto.spec.SecretKeySpec; | ||
import java.lang.reflect.Method; | ||
import java.net.URLEncoder; | ||
import java.nio.ByteBuffer; | ||
import java.nio.charset.StandardCharsets; | ||
import java.security.SecureRandom; | ||
import java.security.*; | ||
import java.security.spec.ECGenParameterSpec; | ||
import java.security.spec.X509EncodedKeySpec; | ||
import java.time.Clock; | ||
import java.time.Instant; | ||
import java.util.Base64; | ||
|
||
|
@@ -52,12 +63,20 @@ public String toString() { | |
private record V2Envelope(String envelope, byte[] nonce) { | ||
} | ||
|
||
private static final SecureRandom SECURE_RANDOM = new SecureRandom(); | ||
private static final String CLIENT_API_KEY = EnvUtil.getEnv("UID2_E2E_API_KEY"); | ||
private static final String CLIENT_API_SECRET = EnvUtil.getEnv("UID2_E2E_API_SECRET"); | ||
private static final String CLIENT_API_KEY_BEFORE_OPTOUT_CUTOFF = EnvUtil.getEnv("UID2_E2E_API_KEY_OLD"); | ||
private static final String CLIENT_API_SECRET_BEFORE_OPTOUT_CUTOFF = EnvUtil.getEnv("UID2_E2E_API_SECRET_OLD"); | ||
private static final String CLIENT_SIDE_TOKEN_GENERATE_SUBSCRIPTION_ID = EnvUtil.getEnv("UID2_E2E_SUBSCRIPTION_ID"); | ||
private static final String CLIENT_SIDE_TOKEN_GENERATE_SERVER_PUBLIC_KEY = EnvUtil.getEnv("UID2_E2E_SERVER_PUBLIC_KEY"); | ||
private static final String CLIENT_SIDE_TOKEN_GENERATE_ORIGIN = EnvUtil.getEnv("UID2_E2E_ORIGIN"); | ||
private static final String CLIENT_SIDE_TOKEN_GENERATE_INVALID_ORIGIN = EnvUtil.getEnv("UID2_E2E_INVALID_ORIGIN"); | ||
private static final IdentityScope IDENTITY_SCOPE = IdentityScope.valueOf(EnvUtil.getEnv("UID2_E2E_IDENTITY_SCOPE")); | ||
private static final int TIMESTAMP_LENGTH = 8; | ||
private static final int PUBLIC_KEY_PREFIX_LENGTH = 9; | ||
private static final int AUTHENTICATION_TAG_LENGTH_BITS = 128; | ||
private static final int IV_BYTES = 12; | ||
private static final String TC_STRING = "CPhJRpMPhJRpMABAMBFRACBoALAAAEJAAIYgAKwAQAKgArABAAqAAA"; | ||
|
||
private final Type type; | ||
|
@@ -182,6 +201,125 @@ public JsonNode v2TokenGenerateUsingPayload(String payload, boolean asOldPartici | |
return v2DecryptEncryptedResponse(encryptedResponse, envelope.nonce(), getClientApiSecret(asOldParticipant)); | ||
} | ||
|
||
public JsonNode v2ClientSideTokenGenerate(String requestBody, boolean useValidOrigin) throws Exception { | ||
final byte[] serverPublicKeyBytes = base64ToByteArray(CLIENT_SIDE_TOKEN_GENERATE_SERVER_PUBLIC_KEY.substring(PUBLIC_KEY_PREFIX_LENGTH)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a big issue, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a fan because it reduces cognitive overhead, no need to ask: "is this variable intentionally mutable / going to be reassigned later?" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep that's fine too, I don't have very strong opinions on either |
||
|
||
final PublicKey serverPublicKey = KeyFactory.getInstance("EC") | ||
.generatePublic(new X509EncodedKeySpec(serverPublicKeyBytes)); | ||
|
||
final KeyPair keyPair = generateKeyPair(); | ||
final SecretKey sharedSecret = generateSharedSecret(serverPublicKey, keyPair); | ||
|
||
final JsonObject cstgEnvelope = createCstgEnvelope(requestBody, CLIENT_SIDE_TOKEN_GENERATE_SUBSCRIPTION_ID, keyPair.getPublic(), sharedSecret); | ||
|
||
final Request.Builder requestBuilder = new Request.Builder() | ||
.url(getBaseUrl() + "/v2/token/client-generate") | ||
.addHeader("Origin", useValidOrigin ? CLIENT_SIDE_TOKEN_GENERATE_ORIGIN : CLIENT_SIDE_TOKEN_GENERATE_INVALID_ORIGIN) | ||
.post(RequestBody.create(cstgEnvelope.toString(), HttpClient.JSON)); | ||
|
||
final String encryptedResponse = HttpClient.execute(requestBuilder.build(), HttpClient.HttpMethod.POST); | ||
final byte[] decryptedResponse = decrypt(base64ToByteArray(encryptedResponse), sharedSecret); | ||
|
||
return Mapper.OBJECT_MAPPER.readTree(decryptedResponse); | ||
} | ||
|
||
private static KeyPair generateKeyPair() { | ||
final KeyPairGenerator keyPairGenerator; | ||
try { | ||
keyPairGenerator = KeyPairGenerator.getInstance("EC"); | ||
} catch (NoSuchAlgorithmException e) { | ||
throw new RuntimeException(e); | ||
} | ||
final ECGenParameterSpec ecParameterSpec = new ECGenParameterSpec("secp256r1"); | ||
try { | ||
keyPairGenerator.initialize(ecParameterSpec); | ||
} catch (InvalidAlgorithmParameterException e) { | ||
throw new RuntimeException(e); | ||
} | ||
return keyPairGenerator.genKeyPair(); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As @gmsdelmundo mentioned, any way to share this code with Operator (test) code? |
||
private static SecretKey generateSharedSecret(PublicKey serverPublicKey, KeyPair clientKeypair) { | ||
try { | ||
final KeyAgreement ka = KeyAgreement.getInstance("ECDH"); | ||
ka.init(clientKeypair.getPrivate()); | ||
ka.doPhase(serverPublicKey, true); | ||
return new SecretKeySpec(ka.generateSecret(), "AES"); | ||
} catch (NoSuchAlgorithmException | InvalidKeyException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
private static JsonObject createCstgEnvelope(String request, String subscriptionId, PublicKey clientPublicKey, SecretKey sharedSecret) { | ||
final long now = Clock.systemUTC().millis(); | ||
|
||
final byte[] iv = new byte[IV_BYTES]; | ||
SECURE_RANDOM.nextBytes(iv); | ||
|
||
final JsonArray aad = new JsonArray(); | ||
aad.add(now); | ||
|
||
final byte[] payload = encrypt(request.getBytes(StandardCharsets.UTF_8), | ||
iv, | ||
aad.toString().getBytes(StandardCharsets.UTF_8), | ||
sharedSecret); | ||
|
||
final JsonObject body = new JsonObject(); | ||
|
||
body.addProperty("payload", byteArrayToBase64(payload)); | ||
body.addProperty("iv", byteArrayToBase64(iv)); | ||
body.addProperty("public_key", byteArrayToBase64(clientPublicKey.getEncoded())); | ||
body.addProperty("timestamp", now); | ||
body.addProperty("subscription_id", subscriptionId); | ||
|
||
return body; | ||
} | ||
|
||
private static byte[] encrypt(byte[] plaintext, byte[] iv, byte[] aad, SecretKey key) { | ||
final Cipher cipher; | ||
try { | ||
cipher = Cipher.getInstance("AES/GCM/NoPadding"); | ||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(AUTHENTICATION_TAG_LENGTH_BITS, iv); | ||
try { | ||
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec); | ||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
||
cipher.updateAAD(aad); | ||
|
||
try { | ||
return cipher.doFinal(plaintext); | ||
} catch (IllegalBlockSizeException | BadPaddingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public byte[] decrypt(byte[] ciphertext, SecretKey key) { | ||
final Cipher cipher; | ||
try { | ||
cipher = Cipher.getInstance("AES/GCM/NoPadding"); | ||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
||
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(AUTHENTICATION_TAG_LENGTH_BITS, ciphertext, 0, IV_BYTES); | ||
try { | ||
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec); | ||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
||
try { | ||
return cipher.doFinal(ciphertext, IV_BYTES, ciphertext.length - IV_BYTES); | ||
} catch (IllegalBlockSizeException | BadPaddingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public TokenRefreshResponse v2TokenRefresh(IdentityTokens identity) { | ||
return publisherClient.refreshToken(identity); | ||
} | ||
|
@@ -242,7 +380,7 @@ private V2Envelope v2CreateEnvelope(String payload, String secret) throws Except | |
|
||
int nonceLength = 8; | ||
byte[] nonce = new byte[nonceLength]; | ||
new SecureRandom().nextBytes(nonce); | ||
SECURE_RANDOM.nextBytes(nonce); | ||
|
||
byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); | ||
|
||
|
@@ -283,11 +421,11 @@ private byte[] encryptGDM(byte[] b, byte[] secretBytes) throws Exception { | |
return (byte[]) encryptGDMMethod.invoke(clazz, b, null, secretBytes); | ||
} | ||
|
||
private byte[] base64ToByteArray(String str) { | ||
private static byte[] base64ToByteArray(String str) { | ||
return Base64.getDecoder().decode(str); | ||
} | ||
|
||
private String byteArrayToBase64(byte[] b) { | ||
private static String byteArrayToBase64(byte[] b) { | ||
return Base64.getEncoder().encodeToString(b); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do both valid and invalid origins have to be passed in dynamically? Can they be hardcoded instead?