Skip to content

Commit

Permalink
Merge pull request #723 from IABTechLab/mkc-UID2-2765-golang-example
Browse files Browse the repository at this point in the history
Add Go encryption/decryption example
  • Loading branch information
mcollins-ttd authored Oct 3, 2024
2 parents 4ae0b4f + a64ae83 commit 94e9e80
Showing 1 changed file with 247 additions and 0 deletions.
247 changes: 247 additions & 0 deletions docs/getting-started/gs-encryption-decryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ The following code example encrypts requests and decrypts responses using C#. Th

This file requires .NET 7.0. You can use an earlier version if required, but it must be .NET Core 3.0 or later. To change the version, replace the [top-level statements](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) with a Main method and the [using declarations](https://learn.microsoft.com/en-us/cpp/cpp/using-declaration?view=msvc-170) with [using statements](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/using).

</TabItem>
<TabItem value='go' label='Go'>

The following code example encrypts requests and decrypts responses using Go. The required parameters are shown at the bottom of the file, or by running `go run uid2_request.go`.

</TabItem>
</Tabs>

Expand Down Expand Up @@ -577,4 +582,246 @@ else
```

</TabItem>

<TabItem value='go' label='Go'>


```go title="uid2_request.go"
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=`)
}
```
</TabItem>

</Tabs>

0 comments on commit 94e9e80

Please sign in to comment.