---
name: captcha-relay-encryption
description: AES-128-CBC encryption scheme for communicating with Captcha Ruler relay service. Use when implementing a proxy/client that needs to encrypt requests and decrypt responses from the relay.
---

# Captcha Relay Encryption

Implement AES-128-CBC request encryption and response decryption for communicating with the Captcha Ruler relay (aggregation) service.

## Algorithm

AES-128-CBC + PKCS7 padding. Each request generates a fresh random token used to derive keys.

## Token Generation

Generate 32 bytes of cryptographically secure random data, encode as Base64URL (no padding). Result is ~43 characters.

```
token = base64url_no_padding(random_bytes(32))
```

This token is sent as the `Authorization` header value (NOT Bearer, just the raw token).

## Request Encryption (client -> relay)

Key and IV derived from token:

```
key = token[0:16]   (first 16 bytes of token string)
iv  = token[-16:]   (last 16 bytes of token string)
```

Steps:
1. Serialize request body to JSON string
2. PKCS7 pad to 16-byte block boundary
3. AES-CBC encrypt with key and iv
4. Base64 standard encode the ciphertext
5. Wrap as JSON: `{"data": "<base64_ciphertext>"}`

## Response Decryption (relay -> client)

Key and IV use 1-byte offset from token:

```
key = token[1:17]        (bytes 1-16 of token string)
iv  = token[-17:-1]      (bytes len-17 to len-1 of token string)
```

Steps:
1. Parse response JSON, extract `data` field (string)
2. Base64 standard decode
3. AES-CBC decrypt with key and iv
4. Remove PKCS7 padding
5. Parse resulting JSON

## HTTP Request Construction

```
POST <relay_url>
Content-Type: application/json
Authorization: <token>
X-Product-Name: <product_name>

{"data": "<encrypted_base64>"}
```

## Reference Implementation (Go)

```go
package agg

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "fmt"
)

func randomAuthToken() (string, error) {
    b := make([]byte, 32)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return base64.RawURLEncoding.EncodeToString(b), nil
}

func encryptWith0_16(token, plain string) (string, error) {
    if len(token) < 16 {
        return "", fmt.Errorf("token too short")
    }
    key := []byte(token[0:16])
    iv := []byte(token[len(token)-16:])
    encrypted, err := aesCBCEncrypt([]byte(plain), key, iv)
    if err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(encrypted), nil
}

func decryptWith1_17(token, data string) (string, error) {
    if len(token) < 18 {
        return "", fmt.Errorf("token too short")
    }
    key := []byte(token[1:17])
    iv := []byte(token[len(token)-17 : len(token)-1])
    cipherBytes, err := base64.StdEncoding.DecodeString(data)
    if err != nil {
        return "", err
    }
    plain, err := aesCBCDecrypt(cipherBytes, key, iv)
    if err != nil {
        return "", err
    }
    return string(pkcs7Unpad(plain)), nil
}

func aesCBCEncrypt(plain, key, iv []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    plain = pkcs7Pad(plain, block.BlockSize())
    mode := cipher.NewCBCEncrypter(block, iv)
    out := make([]byte, len(plain))
    mode.CryptBlocks(out, plain)
    return out, nil
}

func aesCBCDecrypt(ciphertext, key, iv []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    if len(ciphertext)%block.BlockSize() != 0 {
        return nil, fmt.Errorf("ciphertext not multiple of block size")
    }
    mode := cipher.NewCBCDecrypter(block, iv)
    out := make([]byte, len(ciphertext))
    mode.CryptBlocks(out, ciphertext)
    return out, nil
}

func pkcs7Pad(data []byte, blockSize int) []byte {
    pad := blockSize - len(data)%blockSize
    return append(data, bytes.Repeat([]byte{byte(pad)}, pad)...)
}

func pkcs7Unpad(data []byte) []byte {
    if len(data) == 0 {
        return data
    }
    pad := int(data[len(data)-1])
    if pad > 0 && pad <= len(data) {
        return data[:len(data)-pad]
    }
    return data
}
```

## Key Rules

- Every request MUST use a new random token. Never reuse tokens.
- Request encryption: offset 0 (`token[0:16]`, `token[-16:]`)
- Response decryption: offset 1 (`token[1:17]`, `token[-17:-1]`)
- Token is transmitted as plain `Authorization` header (not Bearer scheme)
- Both encrypted request and response use the same JSON envelope: `{"data": "<base64>"}`
- PKCS7 padding block size is always 16 (AES block size)
- Base64 encoding for ciphertext uses standard encoding (with `+/=`), NOT URL-safe

## Generating in Other Languages

When generating an implementation in Python, Node.js, or other languages:
1. Use the standard library's AES-CBC with 128-bit key
2. Implement PKCS7 padding manually if not provided
3. Token string bytes are used directly as key/IV (no hashing or additional derivation)
4. Ensure Base64 standard encoding (not URL-safe) for the ciphertext
5. Use Base64 URL-safe WITHOUT padding for the token itself
