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
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
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
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
Popular in Announcing
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #javascript
- #code-sync
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









