Skip to content

jedisct1/go-fast

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-fast

A Go implementation of the FAST (Format-preserving encryption And Secure Tokenization) algorithm.

FAST is a format-preserving encryption (FPE) scheme that encrypts data while preserving its format. A 16-byte input encrypts to a 16-byte output, a sequence of decimal digits stays decimal, and so on. It supports arbitrary alphabets (radix 2--256), making it suitable both for raw byte encryption and for encrypting structured tokens like credit card numbers, API keys, or identifiers over restricted character sets.

Features

  • Format-preserving encryption: Output has the same length and alphabet as input
  • Arbitrary radix: Supports alphabets from radix 2 (binary) to 256 (bytes)
  • Cross-language parity: Produces identical ciphertext as the JavaScript and Python FAST implementations
  • Secure: Based on AES with provable security guarantees
  • Fast: Optimized implementation with pre-computed S-boxes and efficient diffusion
  • Deterministic: Same plaintext + key + tweak always produces the same ciphertext
  • Tweak support: Domain separation through optional tweak parameter

Installation

go get github.com/jedisct1/go-fast

Usage

Basic Example

package main

import (
    "fmt"
    "github.com/jedisct1/go-fast"
)

func main() {
    // Create a new FAST cipher with a 16-byte key (AES-128)
    key := []byte("0123456789abcdef")
    cipher, err := fast.NewCipher(key)
    if err != nil {
        panic(err)
    }

    // Encrypt some data
    plaintext := []byte("Hello, World!")
    ciphertext := cipher.Encrypt(plaintext, nil)
    
    fmt.Printf("Plaintext:  %s\n", plaintext)
    fmt.Printf("Ciphertext: %x\n", ciphertext)
    
    // Decrypt it back
    decrypted := cipher.Decrypt(ciphertext, nil)
    fmt.Printf("Decrypted:  %s\n", decrypted)
}

Using Tweaks for Domain Separation

// Different tweaks produce different ciphertexts for the same plaintext
data := []byte("sensitive data")
tweak1 := []byte("domain1")
tweak2 := []byte("domain2")

ciphertext1 := cipher.Encrypt(data, tweak1)
ciphertext2 := cipher.Encrypt(data, tweak2)

// ciphertext1 != ciphertext2

// Must use the same tweak to decrypt
decrypted1 := cipher.Decrypt(ciphertext1, tweak1) // ✓ Correct
decrypted2 := cipher.Decrypt(ciphertext1, tweak2) // ✗ Wrong result

Key Sizes

FAST supports AES-128, AES-192, and AES-256:

// AES-128 (recommended)
key128 := make([]byte, 16)
cipher128, _ := fast.NewCipher(key128)

// AES-192
key192 := make([]byte, 24)
cipher192, _ := fast.NewCipher(key192)

// AES-256
key256 := make([]byte, 32)
cipher256, _ := fast.NewCipher(key256)

Arbitrary Radix (Non-Byte Alphabets)

For encrypting data over smaller alphabets -- decimal digits, hex, alphanumeric characters, base64 -- use NewCipherFromParams with parameters computed for the target radix and word length. Each element in the input slice must be in [0, radix).

package main

import (
    "fmt"
    "github.com/jedisct1/go-fast"
)

func main() {
    key := []byte("0123456789abcdef")

    // Encrypt a 16-digit number using radix 10
    params, err := fast.CalculateRecommendedParams(10, 16)
    if err != nil {
        panic(err)
    }
    cipher, err := fast.NewCipherFromParams(params, key)
    if err != nil {
        panic(err)
    }

    // Input: digits 0-9 as byte values (not ASCII)
    digits := []byte{4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
    encrypted := cipher.Encrypt(digits, nil)

    fmt.Printf("Original:  %v\n", digits)
    fmt.Printf("Encrypted: %v\n", encrypted) // still 16 digits, each in [0,9]

    decrypted := cipher.Decrypt(encrypted, nil)
    fmt.Printf("Decrypted: %v\n", decrypted)
}

The parameterized cipher is fixed to a single (radix, wordLength) pair. Create one cipher per combination and reuse it across calls. Different radixes produce completely independent S-box pools and round schedules, so a radix-10 cipher and a radix-62 cipher sharing the same key will produce unrelated outputs.

Token Encryption

The tokens subpackage scans text for API keys and secrets, encrypts them in place while preserving format, and decrypts them back. It recognizes 28 built-in token patterns from GitHub, Stripe, OpenAI, AWS, Slack, SendGrid, and others.

package main

import (
    "fmt"
    "log"

    "github.com/jedisct1/go-fast/tokens"
)

func main() {
    key := []byte("0123456789abcdef") // AES-128

    enc, err := tokens.New(key)
    if err != nil {
        log.Fatal(err)
    }

    text := "GitHub PAT: ghp_ABCDEFghijklmnopqrstuvwxyz0123456789"

    encrypted, err := enc.Encrypt(text)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(encrypted) // "GitHub PAT: ghp_<encrypted-body>"

    decrypted, err := enc.Decrypt(encrypted)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(decrypted == text) // true
}

Per-call options let you filter by token type or override the tweak:

// Encrypt only GitHub tokens, leave others unchanged
encrypted, _ := enc.Encrypt(text, tokens.WithTypes("github-pat"))

// Use a per-call tweak for domain separation
encrypted, _ := enc.Encrypt(text, tokens.WithCallTweak([]byte("production")))

EncryptWithSpans returns per-token metadata, and EncryptWithMappings returns deduplicated plaintext/ciphertext pairs. Token names, alphabets, and ordering match the JavaScript and Python FAST implementations exactly.

Algorithm Details

FAST is based on the research paper:

"FAST: Secure and High Performance Format-Preserving Encryption and Tokenization"
https://eprint.iacr.org/2021/1171.pdf

Key Properties

  • Security: Provides 128-bit security when used with AES-128
  • Performance: Optimized with cached S-boxes and efficient buffer management
  • Format preservation: Input length = output length, values stay within the alphabet
  • Deterministic: Reproducible encryption for the same inputs
  • Two construction modes: NewCipher(key) for byte data of any length; NewCipherFromParams(params, key) for a fixed radix and word length

Security Considerations

  • Use a cryptographically secure random key
  • Different applications should use different tweaks
  • The same (plaintext, key, tweak) always produces the same ciphertext
  • For probabilistic encryption, include random data in the tweak

Testing

Run the comprehensive test suite:

go test -v ./...

For performance benchmarks:

go test -bench=. -benchtime=10s -run=^$

This implementation is based on the FAST specification and is provided for research and educational purposes.

References

About

A Go implementation of the FAST (Format-preserving encryption And Secure Tokenization) algorithm.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages