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.
- 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
go get github.com/jedisct1/go-fastpackage 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)
}// 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 resultFAST 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)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.
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.
FAST is based on the research paper:
"FAST: Secure and High Performance Format-Preserving Encryption and Tokenization"
https://eprint.iacr.org/2021/1171.pdf
- 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
- 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
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.