Skip to content

Commit 0cfaf6f

Browse files
committed
add V2APIMigration
1 parent 70acdad commit 0cfaf6f

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import javax.crypto.Cipher;
2+
import javax.crypto.SecretKey;
3+
import javax.crypto.spec.GCMParameterSpec;
4+
import javax.crypto.spec.SecretKeySpec;
5+
import java.io.*;
6+
import java.net.HttpURLConnection;
7+
import java.net.URL;
8+
import java.nio.ByteBuffer;
9+
import java.nio.charset.StandardCharsets;
10+
import java.security.SecureRandom;
11+
import java.time.Instant;
12+
import java.util.Arrays;
13+
import java.util.Base64;
14+
15+
public class V2APIMigration {
16+
17+
public static void main(String[] args) throws Exception {
18+
if (args.length < 2) {
19+
System.out.println("usage: this <base64-client-key> <base64-client-secret>");
20+
return;
21+
}
22+
final String clientKey = args[0];
23+
final String clientSecret = args[1];
24+
ExampleTokenGeneration(clientKey, clientSecret);
25+
ExampleIdentityMap(clientKey, clientSecret);
26+
}
27+
28+
public static void ExampleTokenGeneration(String apiKey, String secretKey) throws Exception {
29+
// documentation: https://github.com/UnifiedID2/uid2docs/blob/main/api/v2/endpoints/post-token-generate.md
30+
String rawData = "{\"email\": \"username@example.com\"}";
31+
V2Request request = makeV2Request(Instant.now(), rawData.getBytes(StandardCharsets.UTF_8), Base64.getDecoder().decode(secretKey));
32+
final URL endpoint = new URL("https://operator-integ.uidapi.com/v2/token/generate");
33+
sendV2Request(request, endpoint, apiKey, secretKey);
34+
}
35+
36+
public static void ExampleIdentityMap(String apiKey, String secretKey) throws Exception {
37+
// documentation: https://github.com/UnifiedID2/uid2docs/blob/main/api/v2/endpoints/post-identity-map.md
38+
String rawData = "{\"email\": [\"username1@example.com\", \"username2@example.com\", \"username3@example.com\"]}";
39+
V2Request request = makeV2Request(Instant.now(), rawData.getBytes(StandardCharsets.UTF_8), Base64.getDecoder().decode(secretKey));
40+
final URL endpoint = new URL("https://operator-integ.uidapi.com/v2/identity/map");
41+
sendV2Request(request, endpoint, apiKey, secretKey);
42+
}
43+
44+
private static void sendV2Request(V2Request request, URL endpoint, String apiKey, String secretKey) throws Exception {
45+
HttpURLConnection conn = (HttpURLConnection) endpoint.openConnection();
46+
conn.setDoInput(true);
47+
conn.setDoOutput(true);
48+
conn.setRequestMethod("POST");
49+
conn.setRequestProperty("Authorization", "bearer " + apiKey);
50+
conn.getOutputStream().write(request.envelope);
51+
int status = conn.getResponseCode();
52+
if (status == 200) {
53+
final InputStream inputStream = conn.getInputStream();
54+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
55+
int bytesRead;
56+
byte[] data = new byte[4096];
57+
while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
58+
buffer.write(data, 0, bytesRead);
59+
}
60+
byte[] responsePayload = decryptV2Response(buffer.toByteArray(), request.nonce, Base64.getDecoder().decode(secretKey));
61+
System.out.println(new String(responsePayload, StandardCharsets.UTF_8));
62+
} else {
63+
throw new RuntimeException("bad status code " + status);
64+
}
65+
}
66+
67+
public static V2Request makeV2Request(Instant now, byte[] payload, byte[] secretKey) {
68+
byte[] nonce = new byte[8];
69+
new SecureRandom().nextBytes(nonce);
70+
ByteBuffer writer = ByteBuffer.allocate(16 + payload.length);
71+
writer.putLong(now.toEpochMilli());
72+
writer.put(nonce);
73+
writer.put(payload);
74+
byte[] encrypted = encryptGCM(writer.array(), null, secretKey);
75+
ByteBuffer request = ByteBuffer.allocate(encrypted.length + 1);
76+
request.put((byte)1);
77+
request.put(encrypted);
78+
return new V2Request(Base64.getEncoder().encode(request.array()), nonce);
79+
}
80+
81+
private static byte[] decryptV2Response(byte[] envelope, byte[] nonce, byte[] secretKey) {
82+
final byte[] envelopeBytes = Base64.getDecoder().decode(envelope);
83+
final byte[] payload = decryptGCM(envelopeBytes, 0, secretKey);
84+
final byte[] receivedNonce = Arrays.copyOfRange(payload, 8, 8 + nonce.length);
85+
if (!Arrays.equals(receivedNonce, nonce)) {
86+
throw new IllegalStateException("nonce mismatch");
87+
}
88+
return Arrays.copyOfRange(payload, 16, payload.length);
89+
}
90+
91+
private static final int GCM_AUTHTAG_LENGTH = 16;
92+
private static final int GCM_IV_LENGTH = 12;
93+
private static byte[] encryptGCM(byte[] b, byte[] iv, byte[] secretBytes) {
94+
try {
95+
final SecretKey k = new SecretKeySpec(secretBytes, "AES");
96+
final Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
97+
if (iv == null) {
98+
iv = new byte[GCM_IV_LENGTH];
99+
new SecureRandom().nextBytes(iv);
100+
}
101+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_AUTHTAG_LENGTH * 8, iv);
102+
c.init(Cipher.ENCRYPT_MODE, k, gcmParameterSpec);
103+
ByteBuffer buffer = ByteBuffer.allocate(b.length + GCM_IV_LENGTH + GCM_AUTHTAG_LENGTH);
104+
buffer.put(iv);
105+
buffer.put(c.doFinal(b));
106+
return buffer.array();
107+
} catch (Exception e) {
108+
throw new RuntimeException("unable to encrypt", e);
109+
}
110+
}
111+
112+
private static byte[] decryptGCM(byte[] encryptedBytes, int offset, byte[] secretBytes) {
113+
try {
114+
final SecretKey k = new SecretKeySpec(secretBytes, "AES");
115+
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_AUTHTAG_LENGTH * 8, encryptedBytes, offset, GCM_IV_LENGTH);
116+
final Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
117+
c.init(Cipher.DECRYPT_MODE, k, gcmParameterSpec);
118+
return c.doFinal(encryptedBytes, offset + GCM_IV_LENGTH, encryptedBytes.length - offset - GCM_IV_LENGTH);
119+
} catch (Exception e) {
120+
throw new RuntimeException("unable to decrypt", e);
121+
}
122+
}
123+
124+
private static class V2Request {
125+
public final byte[] envelope;
126+
public final byte[] nonce;
127+
128+
public V2Request(byte[] envelope, byte[] nonce) {
129+
this.envelope = envelope;
130+
this.nonce = nonce;
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)