AshFeistelCipher
Encrypted integer IDs for Ash resources using Feistel cipher
Database Support: PostgreSQL only (requires AshPostgres data layer and PostgreSQL database)
Overview
Sequential IDs (1, 2, 3...) leak business information. This library provides a declarative DSL to configure Feistel cipher encryption in your Ash resources, transforming sequential integers into non-sequential, unpredictable values automatically via database triggers.
Key Benefits:
- Secure IDs without UUIDs: Hide sequential patterns while keeping efficient integer IDs (stored as bigint) with configurable ID ranges per column
- Automatic encryption: Database triggers handle encryption transparently
- Collision-free: Deterministic one-to-one mapping
For detailed information about the Feistel cipher algorithm, how it works, security properties, and performance benchmarks, see the feistel_cipher library documentation.
Installation
Using igniter (Recommended)
mix igniter.install ash_feistel_cipher
You can customize the installation with the following options:
--repoor-r: Specify an Ecto repo for FeistelCipher to use.--functions-prefixor-p: Specify the PostgreSQL schema where the FeistelCipher functions will be created, defaults topublic.--functions-saltor-s: Specify the constant value used in the Feistel cipher algorithm. A random value is generated by default if not specified. Must be between 0 and 2^31-1.
⚠️ Security Note: A cryptographically random salt is generated by default for each project. This ensures that encryption patterns cannot be analyzed across different projects. Never use the same salt across multiple production projects.
Manual Installation
If you need more control over the installation process, you can install manually:
-
Add
ash_feistel_cipherto your list of dependencies inmix.exs:def deps do[{:ash_feistel_cipher, "~> 0.13.0"}]end -
Fetch the dependencies:
mix deps.get -
Install FeistelCipher separately:
mix igniter.install feistel_cipher --repo MyApp.Repo -
Add
:ash_feistel_cipherto your formatter configuration in.formatter.exs:[import_deps: [:ash_feistel_cipher]]
Usage
Quick Start
Add AshFeistelCipher extension to your Ash resource and use the declarative DSL:
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
Generate the migration:
mix ash.codegen create_post
This creates a migration with database triggers that automatically encrypt seq into id.
Generated Migration Example:
defmodule MyApp.Repo.Migrations.CreatePost do
use Ecto.Migration
def up do
create table(:posts) do
add :seq, :bigserial, null: false
add :id, :bigint, null: false, primary_key: true
add :title, :string, null: false
end
# Automatically generates trigger for seq -> id encryption
execute(
FeistelCipher.up_for_trigger("public", "posts", "seq", "id",
bits: 52,
key: 1_984_253_769,
rounds: 16,
functions_prefix: "public"
)
)
end
def down do
execute(FeistelCipher.down_for_trigger("public", "posts", "seq", "id"))
drop table(:posts)
end
end
How it works:
When you create a record, the database trigger automatically encrypts the sequential seq value:
# Create a post - seq and id are auto-generated
post = MyApp.Post.create!(%{title: "Hello World"})
# => %MyApp.Post{seq: 1, id: 3_141_592_653, title: "Hello World"}
# The encrypted id is deterministic and collision-free
post2 = MyApp.Post.create!(%{title: "Second Post"})
# => %MyApp.Post{seq: 2, id: 2_718_281_828, title: "Second Post"}
# You can query by the encrypted id
MyApp.Post.get!(3_141_592_653)
# => %MyApp.Post{seq: 1, id: 3_141_592_653, title: "Hello World"}
Advanced Examples
Custom ID range with bits:
attributes do
integer_sequence :seq
encrypted_integer_primary_key :id,
from: :seq,
bits: 40 # ~1 trillion IDs (default: 52 = ~4.5 quadrillion)
end
Using any integer attribute with from (e.g. optional postal code):
attributes do
attribute :postal_code, :integer, allow_nil?: true
encrypted_integer :encrypted_postal_code, from: :postal_code, allow_nil?: true
end
DSL Reference
integer_sequence: Auto-incrementing bigserial column
integer_sequence :seq
encrypted_integer: Encrypted integer column
The base form for encrypted columns. Automatically sets writable?: false, generated?: true
encrypted_integer :id, from: :seq, primary_key?: true, allow_nil?: false, public?: true
encrypted_integer :referral_code, from: :seq
encrypted_integer_primary_key: Shorthand for encrypted primary keys
Convenience macro equivalent to encrypted_integer with primary_key?: true, allow_nil?: false, public?: true pre-set.
encrypted_integer_primary_key :id, from: :seq
encrypted_integer_primary_key :id, from: :seq, bits: 40
Common Options for Encrypted Columns:
Required:
from: Integer attribute to encrypt (can be any integer attribute)
Optional (⚠️ Cannot be changed after records are created):
bits(default: 52): Encryption bit size - determines ID range (40 bits = ~1T IDs, 52 bits = ~4.5Q IDs)key: Custom encryption key (auto-generated from table/column names if not provided)rounds(default: 16): Number of Feistel rounds (higher = more secure but slower)functions_prefix(default: "public"): PostgreSQL schema where feistel functions are installed (set via--functions-prefixduring installation)
Important:
allow_nil?on encrypted column must matchfromattribute's nullability- For primary keys, prefer
encrypted_integer_primary_keyfor cleaner syntax
License
MIT