FF1 Format Preserving Encryption Gem
A Ruby implementation of the FF1 Format Preserving Encryption algorithm from NIST SP 800-38G.
Overview
Format Preserving Encryption (FPE) allows you to encrypt data while maintaining its original format. This is particularly useful when you need to encrypt sensitive data like credit card numbers, social security numbers, or other structured data while keeping the same format for compatibility with existing systems.
The FF1 algorithm is one of two methods specified in NIST Special Publication 800-38G for Format-Preserving Encryption.
Features
- Full implementation of NIST FF1 algorithm
- Dual-mode operation: Reversible and Irreversible encryption
- Support for any radix from 2 to 65, 536
- Tweak support for additional security
- GDPR "right to be forgotten" compliance
- Proper input validation and error handling
- Comprehensive test suite
- Thread-safe implementation
Installation
Add this line to your application's Gemfile:
gem 'ff1'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install ff1
Usage
Basic Usage
require 'ff1'
# Create a cipher with a 128-bit key for decimal numbers (radix 10)
key = "\x2B\x7E\x15\x16\x28\xAE\xD2\xA6\xAB\xF7\x15\x88\x09\xCF\x4F\x3C"
# Reversible mode (default) - can decrypt back to original
cipher = FF1::Cipher.new(key, 10, FF1::Modes::REVERSIBLE)
plaintext = "4111111111111111"
ciphertext = cipher.encrypt(plaintext)
decrypted = cipher.decrypt(ciphertext)
puts "#{plaintext} → #{ciphertext} → #{decrypted}"
# => "4111111111111111 → 8224999410799188 → 4111111111111111"
# Irreversible mode - cannot decrypt (for GDPR compliance)
secure_cipher = FF1::Cipher.new(key, 10, FF1::Modes::IRREVERSIBLE)
secure_ciphertext = secure_cipher.encrypt(plaintext)
# secure_cipher.decrypt(secure_ciphertext) # ❌ Raises error
puts "Securely deleted: #{plaintext} → #{secure_ciphertext}"
# => "Securely deleted: 4111111111111111 → 2858907702179518"
Using Tweaks
Tweaks provide additional input to the encryption algorithm for enhanced security:
cipher = FF1::Cipher.new(key, 10)
tweak = "user123"
plaintext = "1234567890"
ciphertext = cipher.encrypt(plaintext, tweak)
decrypted = cipher.decrypt(ciphertext, tweak)
Different Radix Values
# For hexadecimal data (radix 16)
hex_cipher = FF1::Cipher.new(key, 16)
hex_data = "ABCDEF123456"
encrypted_hex = hex_cipher.encrypt(hex_data)
# For binary data (radix 2)
binary_cipher = FF1::Cipher.new(key, 2)
binary_data = "1010101" # Must be long enough to meet domain requirements
encrypted_binary = binary_cipher.encrypt(binary_data)
Key Requirements
The FF1 algorithm supports AES key lengths:
- 128-bit keys (16 bytes)
- 192-bit keys (24 bytes)
- 256-bit keys (32 bytes)
# 128-bit key
key_128 = SecureRandom.bytes(16)
cipher_128 = FF1::Cipher.new(key_128, 10)
# 256-bit key
key_256 = SecureRandom.bytes(32)
cipher_256 = FF1::Cipher.new(key_256, 10)
Domain Size Requirements
For security, the FF1 algorithm requires that radix^length >= 100
. For better security, radix^length >= 1,000,000
is recommended.
Examples:
- Decimal (radix 10): minimum 2 digits, recommended 7+ digits
- Hexadecimal (radix 16): minimum 2 digits, recommended 5+ digits
- Binary (radix 2): minimum 7 bits, recommended 20+ bits
Error Handling
The gem provides comprehensive error handling:
begin
cipher = FF1::Cipher.new(key, 10)
result = cipher.encrypt("123") # Too short
rescue FF1::Error => e
puts "Encryption error: #{e.message}"
end
Common errors:
FF1::Error
: Base error class for all FF1-related errors- Invalid key length
- Invalid radix (must be 2-65536)
- Input too short or empty
- Invalid characters for the specified radix
- Domain size too small
Security Considerations
- Key Management: Use cryptographically secure random keys and protect them appropriately
- Domain Size: Ensure your domain size meets the minimum requirements (preferably >= 1,000,000)
- Tweaks: Use tweaks when possible for additional security
- Input Validation: The gem validates inputs, but ensure your data meets the requirements
Algorithm Details
This implementation follows NIST SP 800-38G specification for the FF1 algorithm:
- Uses 10 rounds of a Feistel network
- Supports any radix from 2 to 65, 536
- Uses AES as the underlying block cipher
- Implements proper padding and round function as specified
Thread Safety
The FF1:: Cipher instances are thread-safe for encryption and decryption operations. However, you should not modify the cipher instance (key, radix) from multiple threads simultaneously.
Contributing
Bug reports and pull requests are welcome on GitHub.
License
The gem is available as open source under the MIT License.