Skip to main content

FAST format-preserving cipher for Python

A pure Python implementation of the FAST cipher, a format-preserving encryption (FPE) scheme designed for tokenizing API keys, credentials, and other structured secrets.

For prefix-based tokens (GitHub, AWS, Stripe, etc.), encrypted output keeps the exact same format (length, prefix, character set) as the originals, so they pass through systems that validate token formats. Heuristic tokens (Fastly, AWS secret keys) are wrapped in a tagged marker since they have no distinguishing prefix.

Fully interoperable with the C, Zig, Go, and JavaScript implementations.

Installation

pip install fast-cipher

Or with uv:

uv add fast-cipher

Quick start

The most common use case is encrypting tokens and API keys found inside a block of text. TokenEncryptor handles scanning, encrypting, and decrypting automatically:

import os
from fast_cipher.tokens import TokenEncryptor

key = os.urandom(32)  # AES-128, AES-192, or AES-256
encryptor = TokenEncryptor(key)

text = """
Here are the credentials for the staging environment:
GitHub token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij
AWS access key: AKIAIOSFODNN7EXAMPLE
Stripe key: sk_live_ABCDEFGHIJKLMNOPQRSTUVWXab
"""

encrypted, mappings = encryptor.encrypt(text)

encrypt() returns a tuple: the encrypted text and a list of TokenMapping objects that record which tokens were found and what they became. Each mapping has plaintext, ciphertext, and pattern_name fields, which is useful for provenance tracking (only reversing tokens you actually encrypted).

For prefix-based tokens the result still looks like valid tokens: same prefixes, same lengths, same character sets, but the secret parts have been replaced with ciphertext. Decryption restores the original text exactly:

decrypted = encryptor.decrypt(encrypted)
assert decrypted == text

Tweaks

A tweak is optional context data that gets mixed into the encryption. The same plaintext encrypted with different tweaks produces different ciphertext, which is useful for binding tokens to a specific user, session, or tenant:

enc_alice, _ = encryptor.encrypt(text, tweak=b"user-alice")
enc_bob, _ = encryptor.encrypt(text, tweak=b"user-bob")

assert enc_alice != enc_bob

# Each can only be decrypted with the matching tweak
assert encryptor.decrypt(enc_alice, tweak=b"user-alice") == text
assert encryptor.decrypt(enc_bob, tweak=b"user-bob") == text

Filtering by token type

If you only want to encrypt certain kinds of tokens and leave the rest as-is, pass a types list with the pattern names you care about:

text = "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij and AKIAIOSFODNN7EXAMPLE"

encrypted, _ = encryptor.encrypt(text, types=["github-pat"])
# The GitHub token is encrypted, but the AWS key is untouched

Supported token types

The following patterns are detected and encrypted out of the box:

Provider Pattern name(s) Prefix
Anthropic anthropic sk-ant-api03-
AWS aws-access-key AKIA
Datadog datadog ddapi_
GitHub github-pat, github-oauth, github-user, github-server, github-refresh ghp_, gho_, ghu_, ghs_, ghr_
GitLab gitlab glpat-
Google google-api AIza
Grafana grafana glc_
HuggingFace huggingface hf_
npm npm npm_
OpenAI openai, openai-legacy sk-proj-, sk-
PyPI pypi pypi-
SendGrid sendgrid SG.
Slack slack-bot, slack-user xoxb-, xoxp-
Stripe stripe-secret-live, stripe-publish-live, stripe-secret-test, stripe-publish-test sk_live_, pk_live_, sk_test_, pk_test_
Supabase supabase sbp_
Twilio twilio SK
Vercel vercel vercel_
Fastly fastly (heuristic, no prefix)
AWS Secret Key aws-secret-key (heuristic, no prefix)

Heuristic patterns don't rely on a prefix. They look for strings with high entropy and mixed character classes, which is how secrets like Fastly tokens and AWS secret keys are typically formatted. Because there is no distinguishing prefix, encrypted output is wrapped in an [ENCRYPTED:<name>] marker. decrypt() will attempt to unwrap anything matching that marker pattern, so avoid feeding text containing literal [ENCRYPTED:...] strings that were not produced by encrypt().

Custom token patterns

You can register your own patterns for tokens that aren't covered by the built-in set. A SimpleTokenPattern works for anything that has a fixed prefix followed by a body with a known alphabet:

from fast_cipher.tokens import ALPHANUMERIC, TokenEncryptor
from fast_cipher.tokens.types import SimpleTokenPattern

my_pattern = SimpleTokenPattern(
    name="myapp-api-key",
    prefix="myapp_",
    body_regex="[A-Za-z0-9]{32}",
    body_alphabet=ALPHANUMERIC,
    min_body_length=32,
)

key = os.urandom(32)
encryptor = TokenEncryptor(key)
encryptor.register(my_pattern)

text = "key: myapp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
encrypted, _ = encryptor.encrypt(text)
decrypted = encryptor.decrypt(encrypted)
assert decrypted == text

Registered patterns take priority over built-in ones.

The available alphabets are DIGITS, HEX_LOWER, ALPHANUMERIC_UPPER, ALPHANUMERIC_LOWER, ALPHANUMERIC, BASE64, and BASE64URL. You can also create your own with Alphabet(name="my-abc", chars="abc...").

Low-level cipher

TokenEncryptor is built on top of FastCipher, which you can use directly when you need format-preserving encryption for arbitrary data. It works on sequences of integers in a given radix (base).

For example, to encrypt an 8-digit decimal number:

from fast_cipher import FastCipher, calculate_recommended_params

params = calculate_recommended_params(radix=10, word_length=8)
key = os.urandom(32)
cipher = FastCipher(params, key)

plaintext = [1, 2, 3, 4, 5, 6, 7, 8]
ciphertext = cipher.encrypt(plaintext)

# Result is still 8 digits, each between 0 and 9
assert len(ciphertext) == 8
assert all(0 <= d < 10 for d in ciphertext)

decrypted = cipher.decrypt(ciphertext)
assert decrypted == plaintext

For raw bytes, use radix 256 with the encrypt_bytes/decrypt_bytes convenience methods:

params = calculate_recommended_params(radix=256, word_length=16)
cipher = FastCipher(params, key)

ciphertext = cipher.encrypt_bytes(b"sensitive data!!")
plaintext = cipher.decrypt_bytes(ciphertext)
assert plaintext == b"sensitive data!!"

Cleanup

When you're done with an encryptor or cipher, call destroy() to invalidate the instance:

encryptor.destroy()

After destroy(), any further calls to encrypt() or decrypt() will raise a RuntimeError. The method overwrites key references with zero-filled placeholders and clears internal state, but Python's garbage collector may retain copies of the original key bytes in memory. For applications that require guaranteed memory scrubbing, use a C-level implementation instead.

Cross-implementation compatibility

Ciphertext produced by any FAST implementation (C, Zig, Go, JavaScript, Python) can be decrypted by any other, as long as the key, radix, word length, and tweak match. This library is tested against the Go test vectors and cross-validated against the JavaScript implementation.

Metadata

Release files for fast-cipher 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fast-cipher 0.3.0
File Size Uploaded Provenance
fast_cipher-0.3.0.tar.gz 31.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for fast-cipher 0.3.0
File Interpreter ABI Platform
fast_cipher-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 56.7 kB

Release files / fast_cipher-0.3.0.tar.gz

Download URL fast_cipher-0.3.0.tar.gz
Size 31.5 kB
Tags Source
SHA-256 checksum
How to use checksums
d15536deeed4b40016fb950506d50c3f7ccd42bfec78d752c8022234353345fb
BLAKE2b-256 checksum
How to use checksums
0deafbe89b03d79dd0096b6e8a9147bb66e3b00dc21b0c94e06cafe2d7299ddd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / fast_cipher-0.3.0-py3-none-any.whl

Download URL fast_cipher-0.3.0-py3-none-any.whl
Size 25.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a07b560bb4c0430f587a6d4ae8b04bd164aa37c3e383841a96b09a16e33ac94c
BLAKE2b-256 checksum
How to use checksums
b28a9feda8aaecc08cce8fa46fb0b666ded29db1d7b0be79b6928e7f7f24e629
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for fast-cipher 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page