From a64ae8314c92005ccc05f5c76c851843faa8a6d2 Mon Sep 17 00:00:00 2001 From: Matt Collins Date: Tue, 1 Oct 2024 15:53:11 +1000 Subject: [PATCH] Add Go encryption/decryption example --- .../gs-encryption-decryption.md | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/docs/getting-started/gs-encryption-decryption.md b/docs/getting-started/gs-encryption-decryption.md index 66192c7d7..c1bc1522c 100644 --- a/docs/getting-started/gs-encryption-decryption.md +++ b/docs/getting-started/gs-encryption-decryption.md @@ -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). + + + +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`. + @@ -577,4 +582,246 @@ else ``` + + + + +```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 '' | go run uid2_request.go + +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 --refresh-token + +Refresh Token Usage example: + go run uid2_request.go https://prod.uidapi.com/v2/token/refresh --refresh-token AAAAAxxJ...(truncated, total 388 chars) v2ixfQv8eaYNBpDsk5ktJ1yT4445eT47iKC66YJfb1s=`) +} +``` + +