Upgrading FeistelCipher

View Source

From v0.14.0 or v0.15.0 to v1.0.0

v0.14.0/v0.15.0 → v1.0.0 is fully backward compatible when using time_bits: 0. The cipher algorithm (HMAC-SHA256) is identical; only the API and PostgreSQL function names changed.

What changed

  • PG function names: feistel_cipherfeistel_cipher_v1, feistel_column_triggerfeistel_trigger_v1. Old functions are left untouched so they coexist during upgrade.
  • bits option renamed to data_bits (default changed from 52 to 38)
  • New time-related options in v1:
    • time_bits (default: 15) for time-based prefix
    • time_bucket (default: 86400)
    • time_offset (default: 0)
    • encrypt_time (default: false)
    • Use time_bits: 0 to keep the old behavior

Steps

  1. Update dependency to ~> 1.0

  2. Run the upgrade task to generate a database migration:

mix feistel_cipher.upgrade
  1. Edit the generated migration to fill in your functions_salt (find it in the migration with timestamp 19730501000000):
def up do
  # Install v1 functions (coexist with old v0.x functions).
  FeistelCipher.up_v1_functions(functions_prefix: "public", functions_salt: YOUR_SALT)
end

def down do
  FeistelCipher.down_v1_functions(functions_prefix: "public")
end
  1. Generate trigger migration(s):

    For Ash users: Run mix ash.codegen --name upgrade_feistel_triggers_to_v1. In the generated migration's up function, replace down_for_trigger with down_for_legacy_trigger to drop legacy triggers. Also in the down function, replace up_for_trigger with up_for_legacy_trigger and bits: with time_bits: 0, data_bits:.

    For plain Ecto users: Create a separate migration to upgrade each trigger from v0.x to v1. For each table using Feistel cipher, drop the old trigger and recreate with v1:

    # bits: N  ->  time_bits: 0, data_bits: N  (old default for bits was 52)
    execute FeistelCipher.down_for_legacy_trigger("public", "posts", "seq", "id")
    execute FeistelCipher.up_for_v1_trigger("public", "posts", "seq", "id",
      time_bits: 0, data_bits: 52, functions_prefix: "public")
  2. (Optional) Drop old v0.x functions in your last migration. Which functions exist depends on the version you're upgrading from:

    # v0.15.0
    execute "DROP FUNCTION IF EXISTS public.feistel_cipher(bigint, int, bigint, int)"
    execute "DROP FUNCTION IF EXISTS public.feistel_column_trigger()"
    
    # v0.14.0
    execute "DROP FUNCTION IF EXISTS public.feistel_encrypt(bigint, int, bigint, int)"
    execute "DROP FUNCTION IF EXISTS public.feistel_column_trigger()"
    
    # v0.13.x or earlier
    execute "DROP FUNCTION IF EXISTS public.feistel(bigint, int, bigint)"
    execute "DROP FUNCTION IF EXISTS public.handle_feistel_encryption()"
  3. Run mix ecto.migrate


From v0.13.x or earlier to v1.0.0

v0.14.0 introduced a breaking change to the cipher algorithm: the round function was hardened from a simple hash to HMAC-SHA256. This means encryption results are different — the same input produces different output.

Impact

  • Existing encrypted data remains valid as long as the old PostgreSQL functions (feistel_cipher) stay in the database.
  • New rows will use feistel_cipher_v1 which produces different encrypted values than the old feistel_cipher for the same input.
  • There are no primary key collisions because both old and new functions are bijective (one-to-one mapping) within their bit range. The old function maps input space A → output space A (a permutation), and the new function maps input space A → output space A (a different permutation). Since each is collision-free independently, mixing them does not cause collisions.
  • However, the 1:1 reversibility property is lost for old rows: calling feistel_cipher_v1(old_encrypted_id) will NOT return the original seq value.

When this matters

  • If your application decrypts IDs (e.g., id → seq lookup using the cipher function), old rows will decrypt incorrectly with the new function.
  • If your application only uses encrypted IDs as opaque identifiers (lookup by id directly), this is not an issue.

Steps

Follow the same steps as "From v0.14.0 or v0.15.0 to v1.0.0" above. The upgrade task works the same way.

If you need to maintain decryption capability for old rows, keep the old feistel_cipher function in the database (skip Step 3) and use it for decrypting pre-upgrade data.