jechol

jechol

FeistelCipher, AshFeistelCipher - Encrypted integer IDs using Feistel cipher

I’m excited to share FeistelCipher and AshFeistelCipher, PostgreSQL-based libraries that provide encrypted integer IDs using the Feistel cipher algorithm.

The Problem

Sequential IDs (1, 2, 3…) expose sensitive business information:

  • Competitors can estimate your growth rate
  • Users can enumerate resources (/posts/1, /posts/2…)
  • Total record counts are revealed

Common solutions have their own issues:

  • UUIDs: Fixed 36 characters for everything - overkill for most use cases
  • Random integers: Collision risks and complex generation logic

Our Solution

FeistelCipher provides a different approach:

  • Store sequential integers internally
  • Expose encrypted integers externally (non-sequential, unpredictable)
  • Adjustable bit size per column: User ID = 40 bits, Post ID = 52 bits
  • Automatic encryption via PostgreSQL triggers

Key Features

  • Deterministic & Collision-free: One-to-one mapping within the bit range
  • Fast: ~4.4μs per encryption (benchmarked on Apple M3 Pro)

Usage

FeistelCipher (Ecto)

Migration:

defmodule MyApp.Repo.Migrations.CreatePosts do
  use Ecto.Migration

  def up do
    create table(:posts) do
      add :seq, :bigserial
      add :title, :string
    end

    execute FeistelCipher.up_for_trigger("public", "posts", "seq", "id")
  end

  def down do
    execute FeistelCipher.down_for_trigger("public", "posts", "seq", "id")
    drop table(:posts)
  end
end

Schema:

defmodule MyApp.Post do
  use Ecto.Schema

  schema "posts" do
    field :seq, :id, read_after_writes: true
    field :title, :string
  end
  
  @derive {Jason.Encoder, except: [:seq]}  # Hide seq in API responses
end

Usage:

%Post{title: "Hello"} |> Repo.insert()
# => %Post{id: 8234567, seq: 1, title: "Hello"}

The seq column auto-increments, and the trigger automatically encrypts it into the id column.

AshFeistelCipher (Ash Framework)

For Ash Framework users, AshFeistelCipher provides a cleaner, declarative syntax:

defmodule MyApp.Post do
  use Ash.Resource,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshFeistelCipher]

  postgres do
    table "posts"
    repo MyApp.Repo
  end

  attributes do
    integer_sequence :seq
    encrypted_integer_primary_key :id, from: :seq
    
    attribute :title, :string, allow_nil?: false
  end
end

Run mix ash.codegen to generate migrations with automatic trigger configuration.

Links

Most Liked

jechol

jechol

Thank you for the excellent feedback! You’ve identified some important points that deserve clarification.

On Primary Key Performance Trade-offs

You’re absolutely right that using the encrypted id as a primary key loses the benefits of a sequential primary key. This is intentional - it’s the same trade-off that UUIDv4 has (random ordering causes B-tree page splits).

However, the library supports an alternative pattern: Keep id as a sequential primary key and encrypt a separate disp_id column for public display:

create table(:posts, primary_key: false) do
  add :id, :bigserial, primary_key: true    # Sequential, internal
  add :disp_id, :bigint                      # Encrypted, external
  add :title, :string
end

execute FeistelCipher.up_for_trigger("public", "posts", "id", "disp_id")

This gives you sequential PK performance while still hiding growth patterns externally.

Regarding encryption overhead: The encryption takes microseconds while typical INSERT/UPDATE operations involving disk writes (WAL, index updates) take milliseconds, making the encryption overhead negligible. For high-volume inserts or frequent sequential scans over large datasets, this library may not be the optimal choice.

This library targets typical web applications where security/privacy outweighs marginal insert/update performance. I’ve added a “Performance Considerations” section to the README to make these trade-offs explicit.

On Default Salt

You’re 100% correct - this is a security issue. Having all projects share the same default salt means analyzing one project’s encryption could compromise others.

I’ve just released v0.13.0 that automatically generates a unique random salt during installation. Each project now gets its own salt without any manual intervention.

Thanks again for taking the time to review this thoroughly!

jechol

jechol

I prefer systems with mathematical guarantees over probabilistic ones.

Random IDs require more bits to keep collision probability acceptable. Feistel’s collision-free guarantee allows fewer bits for human-friendly short IDs.

The deterministic nature also provides reproducible seed data with stable URLs, which random IDs can’t offer.

Regarding the Ecto Type approach: that would create a mismatch between DB values and URL values, making debugging with tools like TablePlus more difficult since you’d need to decrypt IDs to query the database.

pawoc50825

pawoc50825

This may be interesting to you, introduced just a month ago.

uuidv47.stateless.me

  • v7 in your DB, v4 on the wire
  • UUIDv7 is time-ordered → better index locality & pagination
  • façade hides timing patterns and looks like v4 to clients
  • uses a PRF (SipHash-2-4); avoids non-crypto hashes

github.com/stateless-me/uuidv47

byu

byu

Wouldn’t we still want/need a secondary index for disp_id to lookup – assuming growing number of rows – the record on an outside request?

So that’s still more work on top of the serial PK?

garrison

garrison

BTW UUIDs are not “36 characters”, they are 16 bytes. Nobody is storing the base16 encoding in the DB. It’s for humans.

Where Next?

Popular in Announcing Top

martosaur
Hello, I’m excited to introduce InstructorLite – a fork of the Instructor package. Instructor brought the very idea of structured LLM p...
New
dimamik
Vault is a lightweight Elixir library for immutable data storage within a process subtree. Due to Elixir’s actor model nature, it’s comm...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
mikehostetler
I’m falling in love with the Req Plugin pattern. It has limits, but when it works - it’s good. I ported over my Tesla based Fly Machines...
New
garrison
Hobbes is a scalable, fault-tolerant transactional record store written in Elixir. Hobbes is designed to be: Scalable - Hobbes can sha...
New
sevensidedmarble
Announcing Live Toast: a replacement toast/flash component for Phoenix LiveView, heavily inspired by the look of Sonner (the amazing toas...
New
lud
Hello! I’ve been working on the Oaskit library for a while now, and just released a first version. Since I’ve built JSV I wanted to be ...
New
Antrater
Hi there! At Moon Design System, we have been working hard for the past six months on the next generation of our LiveView component libra...
New
Asd
Hi, I am happy to release the Repatch library for mocking and patching implementation in tests and anywhere else. It brings new possibili...
New
volcov
Hello, How is everyone? I hope you’re well =) I worked on a project with some legacy code that existed before Oban’s arrival. This leg...
New

Other popular topics Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

We're in Beta

About us Mission Statement