Skip to content

Repository files navigation

Catchya

A modern, image-based CAPTCHA library for Go with cryptographic security and Redis-backed session storage.

Overview

Catchya generates interactive image-based CAPTCHA challenges where users select images matching a category (cars, traffic lights, buses, bicycles, crosswalks). Each challenge is cryptographically signed, stored in Redis with TTL expiration, and can only be verified once.

Architecture

%%{init: {'theme':'dark', 'themeVariables': {'fontSize':'16px'}}}%%
flowchart TB
    Client["Client Browser"]

    subgraph Server["Echo HTTP Server"]
        direction TB
        Handler["Challenge Handler<br/><small>HTTP Request Processing</small>"]

        subgraph Service["Catchya Service"]
            direction LR
            Generator["Challenge Generator<br/><small>Grid & Image Composition</small>"]
            Verifier["Verifier<br/><small>Answer Validation</small>"]
        end

        subgraph Components["Core Components"]
            direction TB
            Crypto["Crypto Layer<br/><small>HMAC-SHA256 Signing</small><br/><small>SHA256 Hashing</small>"]
            Storage["Storage Layer<br/><small>Redis with TTL</small>"]
            ImageLoader["Image Loader<br/><small>Unsplash API / Local</small>"]
            Renderer["Grid Renderer<br/><small>PNG Compositor</small>"]
        end
    end

    Client ==>|"1. POST /api/challenge<br/>{grid_size: 3}"| Handler
    Handler --> Generator
    Generator -.->|Generate ID| Crypto
    Generator -.->|Store Challenge| Storage
    Generator -.->|Fetch Images| ImageLoader
    Generator -.->|Compose Grid| Renderer
    Handler ==>|"2. Challenge ID + Image"| Client

    Client ==>|"3. POST /api/verify<br/>{id, selection: [0,2,5,7]}"| Handler
    Handler --> Verifier
    Verifier -.->|Verify Hash| Crypto
    Verifier -.->|Get/Update Challenge| Storage
    Handler ==>|"4. success: true/false"| Client

    classDef clientStyle fill:#1e3a5f,stroke:#4a7ba7,stroke-width:3px,color:#ffffff
    classDef serverStyle fill:#1a1a2e,stroke:#2d2d44,stroke-width:2px,color:#e0e0e0
    classDef serviceStyle fill:#2a2d3a,stroke:#3a3f5c,stroke-width:2px,color:#e0e0e0
    classDef componentStyle fill:#363b4a,stroke:#4a5568,stroke-width:2px,color:#e0e0e0
    classDef cryptoStyle fill:#3d1f3d,stroke:#6b3d6b,stroke-width:2px,color:#ffffff
    classDef storageStyle fill:#1f3d3d,stroke:#3d6b6b,stroke-width:2px,color:#ffffff

    class Client clientStyle
    class Server serverStyle
    class Service serviceStyle
    class Components componentStyle
    class Handler,Generator,Verifier serviceStyle
    class Crypto cryptoStyle
    class Storage storageStyle
    class ImageLoader,Renderer componentStyle
Loading

How It Works

Challenge Generation Flow

sequenceDiagram
    participant C as Client
    participant H as Handler
    participant G as Generator
    participant L as ImageLoader
    participant R as Renderer
    participant Cry as Crypto
    participant S as Redis

    C->>H: POST /api/challenge {grid_size: 3}
    H->>G: Generate(opts)

    G->>G: Select random category

    alt Unsplash API Key Set
        G->>L: Load images from Unsplash API
    else No API Key
        G->>L: Load local/placeholder images
    end

    G->>G: Generate grid (3x3, 4x4, 5x5)
    Note over G: Randomly position target images<br/>Fill remaining with distractors

    G->>R: Render(images)
    R-->>G: Composite PNG image

    G->>Cry: GenerateChallengeID()
    Cry-->>G: HMAC-SHA256 signed ID

    G->>Cry: HashAnswer(correctCells)
    Cry-->>G: SHA256 hash

    G->>S: Store challenge data (TTL: 5min)
    Note over S: Key: catchya:{id}<br/>Data: {category, correctCells,<br/>answerHash, expiresAt, used}

    H-->>C: {challenge_id, category, grid_size, prompt}
    C->>H: GET /api/image/{id}
    H-->>C: PNG image
Loading

Verification Flow

sequenceDiagram
    participant C as Client
    participant H as Handler
    participant V as Verifier
    participant Cry as Crypto
    participant S as Redis

    C->>H: POST /api/verify {id, selection: [0,2,5,7]}
    H->>V: Verify(challengeID, selection)

    V->>S: Get challenge data

    alt Challenge not found
        S-->>V: Error
        V-->>H: "Challenge expired or invalid"
        H-->>C: {success: false}
    end

    V->>V: Check if already used
    alt Already used
        V-->>H: "Challenge already used"
        H-->>C: {success: false}
    end

    V->>V: Check expiration time
    alt Expired
        V-->>H: "Challenge expired"
        H-->>C: {success: false}
    end

    V->>V: Count correct selections
    Note over V: 70% threshold + max 1 false positive

    alt Pass (≥70% correct, ≤1 wrong)
        V->>S: Mark challenge as used
        V-->>H: true
        H-->>C: {success: true}
    else Fail
        V-->>H: false
        H-->>C: {success: false}
    end
Loading

Security Features

Cryptographic Challenge IDs

  • HMAC-SHA256 signed with server secret key
  • Contains timestamp + 16 random bytes
  • Base64-URL encoded for safe transmission
  • Prevents forgery, tampering, and ID prediction

Answer Hashing

  • Answers stored as SHA256 hashes in Redis
  • Never stored in plaintext
  • Prevents answer leakage from storage layer
  • Even database compromise doesn't reveal answers

One-Time Use Protection

  • Challenges marked as used: true after first verification
  • Cannot be replayed or reused
  • Prevents answer sharing between attempts

Time-Limited Sessions

  • Redis TTL expires challenges (default: 5 minutes)
  • Automatic cleanup of stale challenges
  • No manual garbage collection needed

Lenient Verification (70% Threshold)

  • Users need ≥70% correct selections
  • Allows up to 1 false positive (wrong selection)
  • Balances security with usability
  • Example: 4 correct cells → need 3+ right, max 1 wrong

Quick Start

Prerequisites

  • Go 1.23+
  • Docker & Docker Compose (for Redis)
  • (Optional) Unsplash API key for real images

Installation

  1. Clone and setup:
git clone <repo>
cd captcha_lib
go mod tidy
  1. Start Redis:
docker-compose up -d
  1. Run server:
go run cmd/catchya/main.go
  1. Open demo: http://localhost:8080/demo/index.html

Using Unsplash API (Optional)

To use real images from Unsplash instead of placeholders:

  1. Get API key from https://unsplash.com/developers
  2. Create .env file:
UNSPLASH_API_KEY=your_access_key_here
  1. Run server (automatically loads .env):
go run cmd/catchya/main.go

Or use environment variable directly:

# Windows CMD
set UNSPLASH_API_KEY=your_key
go run cmd/catchya/main.go

# PowerShell
$env:UNSPLASH_API_KEY="your_key"
go run cmd/catchya/main.go

# Unix/Linux/macOS
export UNSPLASH_API_KEY=your_key
go run cmd/catchya/main.go

API Endpoints

Generate Challenge

POST /api/challenge
Content-Type: application/json

{
  "grid_size": 3
}

Response:

{
  "challenge_id": "dGltZXN0YW1wOnJhbmRvbTpzaWduYXR1cmU=",
  "category": "cars",
  "grid_size": 3,
  "instruction": "Select all squares with cars"
}

Get Challenge Image

GET /api/image/:challenge_id

Returns: PNG image (binary)

Verify Solution

POST /api/verify
Content-Type: application/json

{
  "challenge_id": "dGltZXN0YW1wOnJhbmRvbTpzaWduYXR1cmU=",
  "selection": [0, 2, 5, 7]
}

Response:

{
  "success": true
}

Configuration

Environment variables:

Variable Default Description
PORT 8080 Server port
REDIS_ADDR localhost:6379 Redis address
REDIS_PASSWORD "" Redis password
SECRET_KEY catchya-secret-key-change-in-production HMAC signing key
DATASET_PATH "" Local image dataset path
UNSPLASH_API_KEY "" Unsplash API access key

Project Structure

catchya/
├── cmd/
│   └── catchya/
│       └── main.go              # Server entry point
├── server/
│   ├── config.go                # Configuration loader
│   ├── server.go                # Echo server setup
│   └── handlers/
│       └── challenge.go         # HTTP handlers
├── crypto/
│   ├── crypto.go                # HMAC & hashing
│   └── crypto_test.go
├── images/
│   ├── loader.go                # Local image loader
│   ├── api_loader.go            # Unsplash API loader
│   ├── generator.go             # Placeholder generator
│   └── processor.go             # Image processing
├── render/
│   ├── grid.go                  # Grid composition
│   └── grid_test.go
├── storage/
│   ├── storage.go               # Storage interface
│   ├── redis.go                 # Redis implementation
│   └── redis_test.go
├── demo/
│   ├── index.html               # Demo page
│   ├── style.css
│   └── app.js
├── types.go                     # Core types
├── challenge.go                 # Challenge generation
├── verify.go                    # Verification logic
├── captcha.go                   # Main service
└── docker-compose.yml           # Redis container

Integration Example

package main

import (
    "github.com/pixperk/catchya"
)

func main() {
    service, err := catchya.New(catchya.Config{
        RedisAddr:      "localhost:6379",
        SecretKey:      "your-secret-key",
        UnsplashAPIKey: "your-unsplash-key", // Optional
    })
    if err != nil {
        panic(err)
    }
    defer service.Close()

    // Generate challenge
    challenge, err := service.Generate(catchya.Options{
        GridSize:    3,
        Category:    "cars",
        TargetCount: 4,
        CellSize:    200,
    })
    if err != nil {
        panic(err)
    }

    // Verify user selection
    success, err := service.Verify(challenge.ID, []int{0, 2, 5, 7})
    if err != nil {
        panic(err)
    }

    if success {
        println("✓ CAPTCHA verified!")
    }
}

Testing

Run all tests:

go test ./...

Run with coverage:

go test -cover ./...

Run specific package:

go test ./crypto
go test ./storage
go test ./render

Image Categories

Supported categories:

  • cars - Automobiles, vehicles
  • traffic_lights - Traffic signals
  • buses - Public transport buses
  • bicycles - Bikes and cycling
  • crosswalks - Pedestrian crossings

Development

Adding New Category

  1. Add to images/generator.go placeholder generation
  2. Add to images/loader.go category list
  3. Add to images/api_loader.go query mapping

Custom Image Loader

Implement the ImageLoader interface:

type ImageLoader interface {
    Load() error
    GetCategories() []string
    GetImages(category string) ([][]byte, error)
    GetDistractorImages(excludeCategory string) [][]byte
    GetRandomImages(category string, count int) ([][]byte, error)
}

Custom Storage Backend

Implement the Storage interface:

type Storage interface {
    Save(challengeID string, data ChallengeData, ttl time.Duration) error
    Get(challengeID string) (*ChallengeData, error)
    Update(challengeID string, data ChallengeData) error
    Delete(challengeID string) error
    Close() error
}

Performance

  • Challenge generation: ~100-200ms (with API), ~10-20ms (local)
  • Verification: ~5-10ms
  • Redis operations: ~1-5ms
  • Grid rendering: ~50-100ms (3x3 grid, 200px cells)

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.

Credits

Built with:

About

image-grid captcha library for go

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages