Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions examples/bank_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Example: Format-preserving encryption of bank account numbers.

Encrypts bank account and routing numbers while preserving:
- Numeric format
- Length
- Common formatting (spaces, dashes)
"""

import ffx


def encrypt_account_number(account: str, ffx_obj) -> str:
"""Encrypt a bank account number.

Args:
account: Account number (digits only or with formatting)
ffx_obj: FFX encrypter configured with radix=10

Returns:
Encrypted account number with same length
"""
digits = ''.join(c for c in account if c.isdigit())

if len(digits) < 4:
raise ValueError("Account number too short")

plain = ffx.FFXInteger(digits, radix=10, blocksize=len(digits))
encrypted = ffx_obj.encrypt(0, plain)

return str(encrypted).zfill(len(digits))


def decrypt_account_number(encrypted: str, ffx_obj) -> str:
"""Decrypt a bank account number."""
cipher = ffx.FFXInteger(encrypted, radix=10, blocksize=len(encrypted))
decrypted = ffx_obj.decrypt(0, cipher)
return str(decrypted).zfill(len(encrypted))


def encrypt_routing_number(routing: str, ffx_obj) -> str:
"""Encrypt a 9-digit ABA routing number."""
digits = ''.join(c for c in routing if c.isdigit())

if len(digits) != 9:
raise ValueError("Routing number must be 9 digits")

plain = ffx.FFXInteger(digits, radix=10, blocksize=9)
encrypted = ffx_obj.encrypt(0, plain)

return str(encrypted).zfill(9)


def decrypt_routing_number(encrypted: str, ffx_obj) -> str:
"""Decrypt a routing number."""
cipher = ffx.FFXInteger(encrypted, radix=10, blocksize=9)
decrypted = ffx_obj.decrypt(0, cipher)
return str(decrypted).zfill(9)


def encrypt_iban(iban: str, ffx_obj_alpha, ffx_obj_num) -> str:
"""Encrypt an IBAN (International Bank Account Number).

Preserves the country code (first 2 letters) and encrypts the rest.
"""
clean = iban.upper().replace(' ', '')
country = clean[:2] # Preserve country code
rest = clean[2:]

# Separate letters and digits
result = country
for char in rest:
if char.isdigit():
plain = ffx.FFXInteger(char, radix=10, blocksize=1)
enc = ffx_obj_num.encrypt(0, plain)
result += str(enc)
elif char.isalpha():
plain = ffx.FFXInteger(char.lower(), radix=36, blocksize=1)
enc = ffx_obj_alpha.encrypt(0, plain)
result += str(enc).upper()

# Format with spaces every 4 characters
return ' '.join(result[i:i+4] for i in range(0, len(result), 4))


def decrypt_iban(encrypted: str, ffx_obj_alpha, ffx_obj_num) -> str:
"""Decrypt an IBAN."""
clean = encrypted.upper().replace(' ', '')
country = clean[:2]
rest = clean[2:]

result = country
for char in rest:
if char.isdigit():
cipher = ffx.FFXInteger(char, radix=10, blocksize=1)
dec = ffx_obj_num.decrypt(0, cipher)
result += str(dec)
elif char.isalpha():
cipher = ffx.FFXInteger(char.lower(), radix=36, blocksize=1)
dec = ffx_obj_alpha.decrypt(0, cipher)
result += str(dec).upper()

return ' '.join(result[i:i+4] for i in range(0, len(result), 4))


def main():
key = ffx.FFXInteger('2b7e151628aed2a6abf7158809cf4f3c', radix=16, blocksize=32)
ffx_num = ffx.new(key.to_bytes(16), radix=10)
ffx_alpha = ffx.new(key.to_bytes(16), radix=36)

print("Bank Account Format-Preserving Encryption")
print("=" * 60)

# Account numbers
accounts = ["1234567890", "9876543210123", "00001111222233"]
print("\n--- Account Numbers ---")
for acct in accounts:
encrypted = encrypt_account_number(acct, ffx_num)
decrypted = decrypt_account_number(encrypted, ffx_num)
print(f"Original: {acct:20} → Encrypted: {encrypted:20} → Verified: {'✓' if acct == decrypted else '✗'}")

# Routing numbers
routings = ["021000021", "121042882", "322271627"]
print("\n--- Routing Numbers ---")
for routing in routings:
encrypted = encrypt_routing_number(routing, ffx_num)
decrypted = decrypt_routing_number(encrypted, ffx_num)
print(f"Original: {routing} → Encrypted: {encrypted} → Verified: {'✓' if routing == decrypted else '✗'}")

# IBANs
ibans = ["DE89 3704 0044 0532 0130 00", "GB82 WEST 1234 5698 7654 32"]
print("\n--- IBANs ---")
for iban in ibans:
encrypted = encrypt_iban(iban, ffx_alpha, ffx_num)
decrypted = decrypt_iban(encrypted, ffx_alpha, ffx_num)
print(f"Original: {iban}")
print(f"Encrypted: {encrypted}")
print(f"Verified: {'✓' if iban == decrypted else '✗'}")
print()


if __name__ == "__main__":
main()
75 changes: 75 additions & 0 deletions examples/credit_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Example: Format-preserving encryption of credit card numbers.

Encrypts 16-digit credit card numbers while preserving:
- The format (groups of 4 digits)
- The length (16 digits)
- Numeric-only output
"""

import ffx


def encrypt_credit_card(card_number: str, ffx_obj) -> str:
"""Encrypt a credit card number, preserving format.

Args:
card_number: Card number (13-19 digits, with or without dashes/spaces)
ffx_obj: FFX encrypter configured with radix=10

Returns:
Encrypted card number with same length
"""
# Remove formatting, keep only digits
digits = ''.join(c for c in card_number if c.isdigit())

if len(digits) < 13 or len(digits) > 19:
raise ValueError(f"Credit card must be 13-19 digits, got {len(digits)}")

plain = ffx.FFXInteger(digits, radix=10, blocksize=len(digits))
encrypted = ffx_obj.encrypt(0, plain)

# Return with standard formatting (groups of 4)
result = str(encrypted).zfill(len(digits))
return '-'.join(result[i:i+4] for i in range(0, len(result), 4))


def decrypt_credit_card(encrypted_card: str, ffx_obj) -> str:
"""Decrypt a credit card number."""
digits = ''.join(c for c in encrypted_card if c.isdigit())

cipher = ffx.FFXInteger(digits, radix=10, blocksize=len(digits))
decrypted = ffx_obj.decrypt(0, cipher)

result = str(decrypted).zfill(len(digits))
return '-'.join(result[i:i+4] for i in range(0, len(result), 4))


def main():
key = ffx.FFXInteger('2b7e151628aed2a6abf7158809cf4f3c', radix=16, blocksize=32)
ffx_obj = ffx.new(key.to_bytes(16), radix=10)

cards = [
"4111-1111-1111-1111", # Test Visa
"5500-0000-0000-0004", # Test Mastercard
"3400-000000-00009", # Test Amex (will be reformatted)
"6011-0000-0000-0004", # Test Discover
]

print("Credit Card Format-Preserving Encryption")
print("=" * 50)

for card in cards:
encrypted = encrypt_credit_card(card, ffx_obj)
decrypted = decrypt_credit_card(encrypted, ffx_obj)
original_digits = ''.join(c for c in card if c.isdigit())
decrypted_digits = ''.join(c for c in decrypted if c.isdigit())

print(f"\nOriginal: {card}")
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {decrypted}")
print(f"Verified: {'✓' if original_digits == decrypted_digits else '✗'}")


if __name__ == "__main__":
main()
85 changes: 85 additions & 0 deletions examples/date_of_birth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Example: Format-preserving encryption of dates.

Encrypts dates while preserving:
- The format (YYYY-MM-DD, MM/DD/YYYY, etc.)
- Valid-looking date structure
- Numeric content

Note: Encrypted dates may not be calendar-valid dates, but will
have the same format and numeric structure.
"""

import re

import ffx


def encrypt_date(date: str, ffx_obj) -> str:
"""Encrypt a date, preserving format.

Args:
date: Date string in any format (YYYY-MM-DD, MM/DD/YYYY, etc.)
ffx_obj: FFX encrypter configured with radix=10

Returns:
Encrypted date with same format
"""
# Split into digit groups and separators
parts = re.split(r'(\d+)', date)

result = []
for part in parts:
if part.isdigit():
plain = ffx.FFXInteger(part, radix=10, blocksize=len(part))
encrypted = ffx_obj.encrypt(0, plain)
result.append(str(encrypted).zfill(len(part)))
else:
result.append(part)

return ''.join(result)


def decrypt_date(encrypted_date: str, ffx_obj) -> str:
"""Decrypt a date."""
parts = re.split(r'(\d+)', encrypted_date)

result = []
for part in parts:
if part.isdigit():
cipher = ffx.FFXInteger(part, radix=10, blocksize=len(part))
decrypted = ffx_obj.decrypt(0, cipher)
result.append(str(decrypted).zfill(len(part)))
else:
result.append(part)

return ''.join(result)


def main():
key = ffx.FFXInteger('2b7e151628aed2a6abf7158809cf4f3c', radix=16, blocksize=32)
ffx_obj = ffx.new(key.to_bytes(16), radix=10)

dates = [
"1990-05-15", # ISO format
"05/15/1990", # US format
"15.05.1990", # European format
"2000-01-01", # Y2K
"12/31/1999", # Pre-Y2K
]

print("Date Format-Preserving Encryption")
print("=" * 50)

for date in dates:
encrypted = encrypt_date(date, ffx_obj)
decrypted = decrypt_date(encrypted, ffx_obj)

print(f"\nOriginal: {date}")
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {decrypted}")
print(f"Verified: {'✓' if date == decrypted else '✗'}")


if __name__ == "__main__":
main()
Loading