Skip to content

Latest commit

 

History

History
824 lines (627 loc) · 31.8 KB

File metadata and controls

824 lines (627 loc) · 31.8 KB
title description hide_table_of_contents sidebar_position
Encryption and Decryption
UID2 リクエストの暗号化とレスポンスの復号化に関する情報。
false
11

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import Link from '@docusaurus/Link'; import IdentityGenerateResponse from '../snippets/_example-identity-generate-response.mdx';

Encrypting Requests and Decrypting Responses

:::note パブリッシャーで、Client-Side に UID2 を実装している場合、暗号化と復号化は、Prebid.js (UID2 Client-Side Integration Guide for Prebid.js を参照してください) や JavaScript SDK (Client-Side Integration Guide for JavaScript を参照してください) などの実装によって自動的に管理されます。 :::

ほとんどすべての UID2 endpoints では、エンドポイントに送られるリクエストは encrypted され、エンドポイントからのレスポンスは decrypted する必要があります。

唯一の例外は、POST /token/refresh エンドポイントへのリクエストは暗号化する必要がないことです。

UID2 API リクエストの暗号化と各レスポンスの復号化について知っておく必要があるのは、以下のとおりです:

Workflow

UID2 API のリクエストレスポンスワークフローは、以下のステップです:

  1. 入力パラメータを含むリクエストボディを JSON 形式で用意します。
  2. リクエスト JSON を暗号化前リクエストデータエンベローブ でラップします。
  3. AES/GCM/NoPadding アルゴリズムと秘密鍵でエンベローブを暗号化します。
  4. 暗号化リクエストエンベローブ を組み立てます。
  5. 暗号化されたリクエストを送信し、暗号化されたレスポンスを受信します。
  6. 暗号化レスポンスエンベローブ を解析します。
  7. レスポンスエンベローブのデータを復号化します。
  8. 得られた 復号化済みレスポンスデータエンベローブ を解析します。
  9. (オプション、推奨) レスポンスエンベローブの nonce がリクエストエンベローブの nonce と一致することを確認します。
  10. 暗号化されていないエンベローブからレスポンス JSON オブジェクトを抽出します。

encrypting requests and decrypting responses のコード例は、Step 2-10 を自動化するのに役立ち、アプリケーションでこれらのステップを実装する方法のリファレンスとなります。

各 UID2 endpoints では、JSON ボディのフォーマットの要件とパラメータを説明し、呼び出し例を含め、復号化されたレスポンスを示しています。以下のセクションでは、暗号化と復号化のコード例、フィールドレイアウトの要件、リクエストとレスポンスの例を示します。

Encrypting Requests

リクエストを暗号化するコードを自分で書くか、UID2 SDK を使うか、提供されているコード例のいずれかを使うかの選択肢があります(Encryption and Decryption Code Examples を参照してください)。自分でコードを書く場合は、unencrypted request data envelopeEncrypted Request Envelope に記載されているフィールドレイアウトの要件に従うようにしてください。

Unencrypted Request Data Envelope

次の表に、リクエスト暗号化コードのフィールドレイアウトを示します。

Offset (Bytes) Size (Bytes) Description
0 8 Unix タイムスタンプ (ミリ秒単位) です。int64 のビッグエンディアンでなければなりません。
8 8 Nonce: リプレイ攻撃から保護するために使用されるランダムな 64 ビットのデータです。対応する 復号化済みレスポンスデータエンベローブ には、レスポンスが有効とみなされるために同じ nonce 値が含まれていなければなりません。
16 N UTF-8 エンコーディングでシリアライズされたリクエスト JSON ドキュメントをペイロードとします。

Encrypted Request Envelope

次の表は、リクエスト暗号化コードのフィールドレイアウトを説明するものです。

Offset (Bytes) Size (Bytes) Description
0 1 エンベローブフォーマットのバージョン。1 でなければなりません。
1 12 96 ビットの初期化ベクトル (IV)、データ暗号化のランダム化に使用されます。
13 N ペイロード(暗号化前リクエストデータエンベローブ) は AES/GCM/NoPadding アルゴリズムで暗号化されます。
13 + N 16 データの整合性を確認するために使用される 128 ビット GCM 認証タグです。

Decrypting Responses

レスポンスを復号化するコードを自分で書くか、UID2 SDKを使うか、提供されているコード例のいずれかを使うかの選択肢があります(Encryption and Decryption Code Examples を参照してください)。独自のコードを書く場合は、Encrypted Response Envelope および Encrypted Response Envelope に記載されているフィールドレイアウトの要件に従うようにしてください。

:::note レスポンスは、サービスが HTTP ステータスコード 200 を返す場合のみ、暗号化されます。 :::

Encrypted Response Envelope

次の表は、レスポンス復号化コードのフィールドレイアウトを説明するものです。

Offset (Bytes) Size (Bytes) Description
0 12 96 ビットの初期化ベクトル (IV)、データ暗号化のランダム化に使用されます。
12 N ペイロード(復号化済みレスポンスデータエンベローブ) は、AES/GCM/NoPadding アルゴリズムで暗号化されています。
12 + N 16 データの整合性を確認するために使用される 128 ビット GCM 認証タグ。

Unencrypted Response Data Envelope

次の表は、レスポンス復号化コードのフィールドレイアウトを説明するものです。

Offset (Bytes) Size (Bytes) Description
0 8 Unix タイムスタンプ (ミリ秒単位) です。int64 のビッグエンディアンでなければなりません。
8 8 Nonce: レスポンスが有効であるとみなされるためには、これは 暗号化前リクエストデータエンベローブ の nonce と一致する必要があります。
16 N UTF-8 エンコーディングでシリアライズされたレスポンス JSON ドキュメントをペイロードとします。

Response Example

例えば、先行例 のメールアドレスに対する POST /token/generate リクエストに対する復号されたレスポンスは、次のようになります:

Encryption and Decryption Code Examples

このセクションには、さまざまなプログラミング言語による暗号化と復号化のコード例が示されています。

POST /token/refresh エンドポイントでは、POST /token/generate または POST /token/refresh へのコールで事前に取得した refresh_tokenrefresh_response_key の値を使用します。

:::note Windows の場合、PowerShell の代わりに Windows コマンドプロンプトを使用している場合は、JSON を囲むシングルクォートも削除する必要があります。例えば、echo {"email": "test@example.com", "optout_check": 1} とします。 :::

Prerequisites and Notes

コードサンプルを使用する前に、使用している言語の前提条件と注意事項を確認してください。

以下のコードサンプルは Python を使ってリクエストを暗号化し、レスポンスを復号化します。必要なパラメータはコード例の一番上に示されており、 python3 uid2_request.py を実行することで得ることができます。

:::note Windowsの場合は python3python に置き換えてください。 :::

Python のコードには pycryptodomexrequests パッケージが必要です。これらは以下のようにしてインストールできます:

pip install pycryptodomex
pip install requests

以下のコードサンプルは、Java を使用してリクエストを暗号化し、レスポンスを復号化します。必要なパラメータは main 関数の先頭に示されています:

java -jar Uid2Request-jar-with-dependencies.jar

Java のサンプルは JDK version 11 以降用に書かれており、クラスパスに com.google.code.gson ライブラリーが必要です。

Maven を使用している場合は、以下の最小限の pom.xml を使用し、mvn package を実行してプロジェクトをビルドすることができます:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>Uid2Request</artifactId>
  <version>1.0</version>
  
  <properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.10</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.4.2</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
              <manifest>
                <mainClass>org.example.Uid2Request</mainClass>
              </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
    <finalName>${artifactId}</finalName>
  </build>
</project>

以下のコードサンプルは、C# を使用してリクエストを暗号化し、レスポンスを復号化します。必要なパラメータはファイルの先頭に記載されています。また、.\uid2_request をビルドして実行することでも確認できます。

このファイルには.NET 7.0が必要です。必要であれば、それ以前のバージョンを使用することもできますが、.NET Core 3.0以降でなければなりません。バージョンを変更するには、top-level statements を Main メソッドに、using 宣言using ステートメント に置き換えてください。

以下のコード例では、Go を使用してリクエストを暗号化し、レスポンスを復号化します。必要なパラメータはファイルの一番下に記載されています。または、go run uid2_request.go を実行して確認できます。

Code Example

使いたいコードサンプルを選んでください。Prerequisites and Notes を忘れずに確認してください。

"""
Usage:
   echo '<json>' | python3 uid2_request.py <url> <api_key> <client_secret>

Example:
   echo '{"email": "test@example.com", "optout_check": 1}' | python3 uid2_request.py https://prod.uidapi.com/v2/token/generate PRODGwJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg= wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=
   

Refresh Token Usage:
   python3 uid2_request.py <url> --refresh-token <refresh_token> <refresh_response_key>

Refresh Token Usage example:
   python3 uid2_request.py https://prod.uidapi.com/v2/token/refresh --refresh-token AAAAAxxJ...(truncated, total 388 chars) v2ixfQv8eaYNBpDsk5ktJ1yT4445eT47iKC66YJfb1s=

"""

import base64
import os
import sys	
import time
import json

import requests
from Cryptodome.Cipher import AES

def b64decode(b64string, param):
   try:
      return base64.b64decode(b64string)
   except Exception:
   	   print(f"Error: <{param}> is not base64 encoded")
   	   sys.exit()
	   
if len(sys.argv) != 4 and len(sys.argv) != 5:
   print(__doc__)
   sys.exit()

url = sys.argv[1]

is_refresh = 1 if sys.argv[2] == '--refresh-token' else 0
if is_refresh:
   refresh_token = sys.argv[3]
   secret = b64decode(sys.argv[4], "refresh_response_key")
   print(f"\nRequest: Sending refresh_token to {url}\n")
   http_response = requests.post(url, refresh_token)
else:
   api_key = sys.argv[2]
   secret = b64decode(sys.argv[3], "client_secret")
   payload = "".join(sys.stdin.readlines())

   iv = os.urandom(12)
   cipher = AES.new(secret, AES.MODE_GCM, nonce=iv)

   millisec = int(time.time() * 1000)
   request_nonce = os.urandom(8)

   print(f"\nRequest: Encrypting and sending to {url} : {payload}")

   body = bytearray(millisec.to_bytes(8, 'big'))
   body += bytearray(request_nonce)
   body += bytearray(bytes(payload, 'utf-8'))

   ciphertext, tag = cipher.encrypt_and_digest(body)

   envelope = bytearray(b'\x01')
   envelope += bytearray(iv)
   envelope += bytearray(ciphertext)
   envelope += bytearray(tag)

   base64Envelope = base64.b64encode(bytes(envelope)).decode()

   http_response = requests.post(url, base64Envelope, headers={"Authorization": "Bearer " + api_key})
   
# Decryption 
response = http_response.content
if http_response.status_code != 200:
   print(f"Response: Error HTTP status code {http_response.status_code}", end=", check api_key\n" if http_response.status_code == 401 else "\n")
   print(response.decode("utf-8"))
else:
   resp_bytes = base64.b64decode(response)
   iv = resp_bytes[:12]
   data = resp_bytes[12:len(resp_bytes) - 16]
   tag = resp_bytes[len(resp_bytes) - 16:]

   cipher = AES.new(secret, AES.MODE_GCM, nonce=iv)
   decrypted = cipher.decrypt_and_verify(data, tag)

   if is_refresh != 1:
      json_resp = json.loads(decrypted[16:].decode("utf-8"))
   else:
      json_resp = json.loads(decrypted.decode("utf-8"))
      
   print("Response JSON:")
   print(json.dumps(json_resp, indent=4))
package org.example;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Uid2Request {
  public static final int NONCE_LENGTH_BYTES = 8;
  private static final int GCM_TAG_LENGTH_BYTES = 16;
  private static final int GCM_IV_LENGTH_BYTES = 12;

  public static void main(String[] args) throws Exception {
    if (args.length != 3 && args.length != 4) {
      System.out.println(
              "Usage:" + "\n   "
      +             "java -jar Uid2Request-jar-with-dependencies.jar <url> <api_key> <client_secret>" + "\n\n"
      
      +       "Example:" + "\n   "  
      +             "echo '{\"email\": \"test@example.com\",\"optout_check\": 1}' | java -jar Uid2Request-jar-with-dependencies.jar https://prod.uidapi.com/v2/token/generate PRODGwJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg= wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=" + "\n\n\n"
      
      +       "Refresh Token Usage:" + "\n   "
      +             "java -jar Uid2Request-jar-with-dependencies.jar <url> --refresh-token <refresh_token> <refresh_response_key>"  + "\n\n"
                      
      +       "Refresh Token Example:" + "\n   " 
      +             "java -jar Uid2Request-jar-with-dependencies.jar https://prod.uidapi.com/v2/token/refresh --refresh-token AAAAAxxJ...(truncated, total 388 chars) v2ixfQv8eaYNBpDsk5ktJ1yT4445eT47iKC66YJfb1s="  + "\n"
      );
      System.exit(1);
    }

    String url = args[0];
    boolean isRefresh = "--refresh-token".equals(args[1]);
    byte[] secret = Base64.getDecoder().decode(args[isRefresh ? 3 : 2]);

    String payload = isRefresh ? args[2] : new BufferedReader(new InputStreamReader(System.in)).readLine();

    HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
    connection.setRequestMethod("POST");

    if (!isRefresh) {
      String apiKey = args[1];

      byte[] iv = new byte[GCM_IV_LENGTH_BYTES];
      long timestamp = Instant.now().toEpochMilli();
      byte[] requestNonce = new byte[NONCE_LENGTH_BYTES];
      byte[] plaintext = payload.getBytes(StandardCharsets.UTF_8);

      SecureRandom secureRandom = new SecureRandom();
      secureRandom.nextBytes(iv);
      secureRandom.nextBytes(requestNonce);

      ByteBuffer body = ByteBuffer.allocate(8 + requestNonce.length + plaintext.length);
      body.putLong(timestamp);
      body.put(requestNonce);
      body.put(plaintext);

      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
      GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, iv);
      cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), parameterSpec);

      ByteBuffer envelope = ByteBuffer.allocate(1 + body.array().length + GCM_IV_LENGTH_BYTES + GCM_TAG_LENGTH_BYTES);
      envelope.put((byte) 1);
      envelope.put(iv);
      envelope.put(cipher.doFinal(body.array()));

      payload = Base64.getEncoder().encodeToString(envelope.array());
      connection.setRequestProperty("Authorization", "Bearer " + apiKey);
    }

    connection.setDoOutput(true);
    try (OutputStream os = connection.getOutputStream()) {
      os.write(payload.getBytes(StandardCharsets.UTF_8));
    }

    // Handle response
    int status = connection.getResponseCode();
    BufferedReader reader = status == 200 ?
        new BufferedReader(new InputStreamReader(connection.getInputStream())) :
        new BufferedReader(new InputStreamReader(connection.getErrorStream()));
    StringBuilder response = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
      response.append(line);
    }
    reader.close();

    if (status != HttpURLConnection.HTTP_OK) {
      System.out.println("Error: HTTP status code " + status);
      System.out.println(response);
      return;
    }

    byte[] respBytes = Base64.getDecoder().decode(response.toString());

    Cipher respCipher = Cipher.getInstance("AES/GCM/NoPadding");
    respCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, respBytes, 0, GCM_IV_LENGTH_BYTES));
    byte[] decrypted = respCipher.doFinal(respBytes, GCM_IV_LENGTH_BYTES, respBytes.length - GCM_IV_LENGTH_BYTES);

    JsonObject jsonResponse;
    if (!isRefresh) {
      jsonResponse = JsonParser.parseString(new String(Arrays.copyOfRange(decrypted, 8 + NONCE_LENGTH_BYTES, decrypted.length), StandardCharsets.UTF_8)).getAsJsonObject();
    } else {
      jsonResponse = JsonParser.parseString(new String(decrypted, StandardCharsets.UTF_8)).getAsJsonObject();
    }
    String prettyJsonResponse = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create().toJson(jsonResponse);
    System.out.println("Response JSON:");
    System.out.println(prettyJsonResponse);
  }
}
using System.Buffers.Binary;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;


if (args.Length != 3 && args.Length != 4)
{
    Console.WriteLine("""
Usage:
   echo '<json>' | .\uid2_request <url> <api_key> <client_secret>

Example:
   echo '{"email": "test@example.com", "optout_check": 1}' | .\uid2_request https://prod.uidapi.com/v2/token/generate UID2-C-L-999-fCXrMM.fsR3mDqAXELtWWMS+xG1s7RdgRTMqdOH2qaAo= wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=
   

Refresh Token Usage:
   .\uid2_request <url> --refresh-token <refresh_token> <refresh_response_key>

Refresh Token Usage example:
   .\uid2_request https://prod.uidapi.com/v2/token/refresh --refresh-token AAAAAxxJ...(truncated, total 388 chars) v2ixfQv8eaYNBpDsk5ktJ1yT4445eT47iKC66YJfb1s=
""");

    Environment.Exit(1);
}

const int GCM_IV_LENGTH = 12;

string url = args[0];
byte[] secret;

HttpResponseMessage? response;
bool isRefresh = args[1] == "--refresh-token";

if (isRefresh)
{
    string refreshToken = args[2];
    secret = Convert.FromBase64String(args[3]);

    Console.WriteLine($"\nRequest: Sending refresh_token to {url}\n");
    using HttpClient httpClient = new HttpClient();
    var content = new StringContent(refreshToken, Encoding.UTF8);
    response = await httpClient.PostAsync(url, content);
}
else
{
    string apiKey = args[1];
    secret = Convert.FromBase64String(args[2]);

    string payload = Console.In.ReadToEnd();

    var request = new HttpRequestMessage(HttpMethod.Post, url);
    request.Headers.Add("Authorization", $"Bearer {apiKey}");

    var unixTimestamp = new byte[8];
    BinaryPrimitives.WriteInt64BigEndian(unixTimestamp, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());

    var nonce = new byte[8];
    var rnd = new Random();
    rnd.NextBytes(nonce);

    var payloadBytes = Encoding.UTF8.GetBytes(payload);

    var unencryptedRequestDataEnvelope = new byte[unixTimestamp.Length + nonce.Length + payloadBytes.Length];
    unixTimestamp.CopyTo(unencryptedRequestDataEnvelope, 0);
    nonce.CopyTo(unencryptedRequestDataEnvelope, unixTimestamp.Length);
    payloadBytes.CopyTo(unencryptedRequestDataEnvelope, unixTimestamp.Length + nonce.Length);

    var iv = new byte[GCM_IV_LENGTH];
    rnd.NextBytes(iv);

    var encryptedPayload = new byte[unencryptedRequestDataEnvelope.Length];
    var tag = new byte[AesGcm.TagByteSizes.MaxSize];
    using AesGcm aesGcm = new AesGcm(secret);
    aesGcm.Encrypt(iv, unencryptedRequestDataEnvelope, encryptedPayload, tag);

    var envelopeMemoryStream = new MemoryStream(1 + iv.Length + encryptedPayload.Length + AesGcm.TagByteSizes.MaxSize);
    envelopeMemoryStream.WriteByte(1); //version of the envelope format
    envelopeMemoryStream.Write(iv);
    envelopeMemoryStream.Write(encryptedPayload);
    envelopeMemoryStream.Write(tag);
    var envelope = Convert.ToBase64String(envelopeMemoryStream.ToArray());

    request.Content = new StringContent(envelope, Encoding.UTF8);

    var client = new HttpClient();
    response = await client.SendAsync(request);
}

var responseStream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(responseStream);

var responseBody = await reader.ReadToEndAsync();

if (response.StatusCode != HttpStatusCode.OK)
{
    Console.WriteLine($"Response: Error HTTP status code {(int)response.StatusCode}" + ((response.StatusCode == HttpStatusCode.Unauthorized) ? ", check api_key" : ""));
    Console.WriteLine(responseBody);
}
else
{
    var encryptedResponseEnvelope = Convert.FromBase64String(responseBody);

    var responseMemoryStream = new MemoryStream(encryptedResponseEnvelope);
    byte[] iv = new byte[GCM_IV_LENGTH];
    responseMemoryStream.Read(iv);

    int encryptedPayloadLength = encryptedResponseEnvelope.Length - GCM_IV_LENGTH - AesGcm.TagByteSizes.MaxSize;
    byte[] encryptedPayload = new byte[encryptedPayloadLength];
    responseMemoryStream.Read(encryptedPayload);

    byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
    responseMemoryStream.Read(tag);

    using AesGcm aesGcm = new AesGcm(secret);
    byte[] unencryptedResponseDataEnvelope = new byte[encryptedPayload.Length];
    aesGcm.Decrypt(iv, encryptedPayload, tag, unencryptedResponseDataEnvelope);

    int offset = isRefresh ? 0 : 16; //8 bytes for timestamp + 8 bytes for nonce
    var json = Encoding.UTF8.GetString(unencryptedResponseDataEnvelope, offset, unencryptedResponseDataEnvelope.Length - offset);

    Console.WriteLine("Response JSON:");
    
    using var jDoc = JsonDocument.Parse(json);
    Console.WriteLine(JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true }));
}
package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const (
	nonceLengthBytes = 8
	gcmIVLengthBytes = 12
)

func main() {
	subArgs := os.Args[1:]

	if len(subArgs) != 3 && len(subArgs) != 4 {
		printUsage()
		os.Exit(1)
	}

	url := subArgs[0]

	response, err := func() (map[string]interface{}, error) {
		if subArgs[1] == "--refresh-token" {
			return refresh(url, subArgs[2], subArgs[3])
		} else {
			return generate(url, subArgs[1], subArgs[2])
		}
	}()

	if err != nil {
		log.Fatal(err)
	}

	prettyPrint(response)
}

func refresh(url string, refreshToken string, refreshResponseKey string) (map[string]interface{}, error) {
	fmt.Printf("Request: Sending refresh_token to %s\n", url)

	response, err := http.Post(url, "", strings.NewReader(refreshToken))
	if err != nil {
		return nil, err
	}

	return deserializeResponse(response, refreshResponseKey, true)
}

func generate(url string, apiKey string, secret string) (map[string]interface{}, error) {
	payload, err := io.ReadAll(os.Stdin)
	if err != nil {
		return nil, err
	}

	key, err := base64.StdEncoding.DecodeString(secret)
	if err != nil {
		return nil, err
	}

	unencryptedEnvelope, err := makeUnencryptedEnvelope(payload)
	if err != nil {
		return nil, err
	}

	envelope, err := makeEncryptedEnvelope(unencryptedEnvelope, key)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", url, strings.NewReader(base64.StdEncoding.EncodeToString(envelope)))
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", "Bearer "+apiKey)

	response, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	return deserializeResponse(response, secret, false)
}

func aesgcm(key []byte) (cipher.AEAD, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	return cipher.NewGCM(block)
}

func decryptResponse(ciphertext string, key string) ([]byte, error) {
	ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext)
	if err != nil {
		return nil, err
	}

	keyBytes, err := base64.StdEncoding.DecodeString(key)
	if err != nil {
		return nil, err
	}

	aesgcm, err := aesgcm(keyBytes)
	if err != nil {
		return nil, err
	}

	iv := ciphertextBytes[:gcmIVLengthBytes]
	return aesgcm.Open(nil, iv, ciphertextBytes[gcmIVLengthBytes:], nil)
}

func deserialize(bytes []byte) (map[string]interface{}, error) {
	var anyJson map[string]interface{}
	err := json.Unmarshal(bytes, &anyJson)
	return anyJson, err
}

func prettyPrint(obj map[string]interface{}) error {
	bytes, err := json.MarshalIndent(obj, "", "  ")
	if err != nil {
		return err
	}

	fmt.Println(string(bytes))
	return nil
}

func checkStatusCode(response *http.Response, body []byte) error {
	if response.StatusCode != http.StatusOK {
		return fmt.Errorf("Response: Error HTTP status code %d\n%s", response.StatusCode, body)
	}

	return nil
}

func makeUnencryptedEnvelope(payload []byte) ([]byte, error) {
	timestamp := make([]byte, 8)
	binary.BigEndian.PutUint64(timestamp, uint64(time.Now().UnixMilli()))

	nonce := make([]byte, nonceLengthBytes)
	_, err := rand.Read(nonce)
	if err != nil {
		return nil, err
	}

	var body bytes.Buffer
	body.Write(timestamp)
	body.Write(nonce)
	body.Write(payload)
	return body.Bytes(), nil
}

func encrypt(plaintext []byte, iv []byte, key []byte) ([]byte, error) {
	aesgcm, err := aesgcm(key)
	if err != nil {
		return nil, err
	}

	return aesgcm.Seal(nil, iv, plaintext, nil), nil
}

func makeEncryptedEnvelope(payload []byte, key []byte) ([]byte, error) {
	iv := make([]byte, gcmIVLengthBytes)
	_, err := rand.Read(iv)
	if err != nil {
		return nil, err
	}

	ciphertext, err := encrypt(payload, iv, key)
	if err != nil {
		return nil, err
	}

	var envelope bytes.Buffer
	envelope.WriteByte(1)
	envelope.Write(iv)
	envelope.Write(ciphertext)
	return envelope.Bytes(), nil
}

func deserializeResponse(response *http.Response, key string, isRefresh bool) (map[string]interface{}, error) {
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}

	err = checkStatusCode(response, body)
	if err != nil {
		return nil, err
	}

	plaintext, err := decryptResponse(string(body), key)
	if err != nil {
		return nil, err
	}

	offset := 16
	if isRefresh {
		offset = 0
	}

	return deserialize(plaintext[offset:])
}

func printUsage() {
	fmt.Println(`Usage:
   echo '<json>' | go run uid2_request.go <url> <api_key> <client_secret>

Example:
   echo '{"email": "test@example.com", "optout_check": 1}' | go run uid2_request.go https://prod.uidapi.com/v2/token/generate UID2-C-L-999-fCXrMM.fsR3mDqAXELtWWMS+xG1s7RdgRTMqdOH2qaAo= wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=
   

Refresh Token Usage:
   go run uid2_request.go <url> --refresh-token <refresh_token> <refresh_response_key>

Refresh Token Usage example:
   go run uid2_request.go https://prod.uidapi.com/v2/token/refresh --refresh-token AAAAAxxJ...(truncated, total 388 chars) v2ixfQv8eaYNBpDsk5ktJ1yT4445eT47iKC66YJfb1s=`)
}