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