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.