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

  1. Key Management: Use cryptographically secure random keys and protect them appropriately
  2. Domain Size: Ensure your domain size meets the minimum requirements (preferably >= 1,000,000)
  3. Tweaks: Use tweaks when possible for additional security
  4. 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.

References