Rules for working with AshFeistelCipher
View SourceOverview
AshFeistelCipher encrypts sequential integer IDs using Feistel cipher to prevent business information leakage. It handles encryption automatically via PostgreSQL database triggers.
Database Support: PostgreSQL only (requires AshPostgres data layer)
Default profile: time_bits: 15, data_bits: 38
Installation
Recommended using igniter:
mix igniter.install ash_feistel_cipher
Key options:
--repoor-r: Specify Ecto repo--functions-prefixor-p: PostgreSQL schema for functions (default:public)--functions-saltor-s: Feistel cipher salt (default: randomly generated)
⚠️ Security Note: A unique salt is automatically generated per project. Never use the same salt across multiple production projects.
Basic Usage
Simple Primary Key Encryption
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
endGenerate migration:
mix ash.codegen create_post
Multiple Encrypted Columns
Create multiple encrypted columns from the same source (each uses different key):
attributes do
integer_sequence :seq
encrypted_integer_primary_key :id, from: :seq
encrypted_integer :referral_code, from: :seq, allow_nil?: false
endOptional Integer Encryption
Nullable integer attributes can also be encrypted:
attributes do
attribute :postal_code, :integer, allow_nil?: true
encrypted_integer :encrypted_postal_code, from: :postal_code, allow_nil?: true
endDSL Reference
integer_sequence
Declares an auto-incrementing bigserial column:
integer_sequence :seqencrypted_integer
Encrypted integer column (automatically sets writable?: false, generated?: true):
encrypted_integer :id, from: :seq, primary_key?: true
encrypted_integer :referral_code, from: :seq, key: 12345
encrypted_integer :public_id, from: :seq, allow_nil?: truedefault: is not supported for encrypted_integer. Values are generated from from:, and encrypted_integer always uses an internal sentinel to avoid bigserial generation.
encrypted_integer_primary_key
Shorthand for primary keys (automatically sets primary_key?: true, allow_nil?: false, public?: true):
encrypted_integer_primary_key :id, from: :seq
encrypted_integer_primary_key :id, from: :seq, time_bits: 15, data_bits: 38Configuration Options
Required
from: Integer attribute to encrypt (required)
Optional
⚠️ Treat changes as explicit migrations:
time_bits(default: 15): Time prefix bits. Set to0for v0.x-compatible behavior.time_bucket(default: 86400): Time bucket size in seconds.encrypt_time(default: false): Whether to encrypt the time prefix.data_bits(default: 38): Data encryption bit size. Must be even.key: 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 installedbackfill?(default: true): Backfill existing rows when adding a new encrypted column. Setbackfill?: falseto leave existing rows at the internal sentinel value until you handle them explicitly.
Custom Bit Size Example
encrypted_integer_primary_key :id,
from: :seq,
time_bits: 15,
data_bits: 40 # ~1 trillion ID rangeImportant Rules
Nullability Consistency
The encrypted column's allow_nil? must match the source attribute:
# CORRECT
attribute :postal_code, :integer, allow_nil?: true
encrypted_integer :encrypted_postal_code, from: :postal_code, allow_nil?: true
# WRONG - will fail verification
attribute :postal_code, :integer, allow_nil?: true
encrypted_integer :encrypted_postal_code, from: :postal_code, allow_nil?: falseSource Attribute Must Exist
The attribute specified in from must be defined:
# WRONG - :seq not defined
attributes do
encrypted_integer_primary_key :id, from: :seq # Error!
end
# CORRECT
attributes do
integer_sequence :seq
encrypted_integer_primary_key :id, from: :seq
endMigration Required For Parameter Changes
These options should be treated as immutable in-place once records exist:
time_bitstime_bucketencrypt_timedata_bitskeyrounds
Changing them requires data migration.
How It Works
Database triggers handle encryption automatically:
post = MyApp.Post.create!(%{title: "Hello"})
# => %MyApp.Post{seq: 1, id: 3_141_592_653, ...}
post2 = MyApp.Post.create!(%{title: "World"})
# => %MyApp.Post{seq: 2, id: 2_718_281_828, ...}- Sequential
seq→ Non-sequentialidvia automatic encryption - Deterministic (same seq always produces same id)
- Collision-free (one-to-one mapping)
Migration
mix ash.codegen automatically includes trigger creation code in migrations:
def up do
create table(:posts) do
add :seq, :bigserial, null: false
add :id, :bigint, null: false, primary_key: true
end
execute(
FeistelCipher.up_for_trigger("public", "posts", "seq", "id",
time_bits: 15,
data_bits: 38,
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)
endTesting
Use standard Ash testing patterns:
test "encrypted IDs are generated" do
post = MyApp.Domain.create_post!(%{title: "Test"})
assert post.id != post.seq
assert post.id > 0
# Deterministic - same seq always produces same id
post2 = MyApp.Domain.create_post!(%{title: "Test2"})
assert post.id != post2.id
endCommon Pitfalls
Using UUID Instead
If you need UUIDs, use Ash's built-in uuid_primary_key:
# Use Feistel for integer IDs
encrypted_integer_primary_key :id, from: :seq
# Use UUID for random IDs
uuid_primary_key :idExposing Sequential IDs
Don't expose sequential IDs directly:
# BAD - exposes sequential pattern
%{id: post.seq}
# GOOD - use encrypted ID
%{id: post.id}Changing Encryption Settings
Cannot change time_bits, time_bucket, encrypt_time, data_bits, key, or rounds after records are created. Requires data migration to change.
Upgrading to v1.1.0
ash_feistel_cipher 1.1.0and this guide now targetfeistel_cipher 1.1.0.- Keep the default profile as
time_bits: 15,data_bits: 38for new resources. - Replace legacy
bits: Nwithtime_bits: 0, data_bits: Nin resource DSL. - Run
mix ash_feistel_cipher.upgradeto generate SQL function migration scaffolding. - Run
mix ash.codegen --name upgrade_feistel_triggers_to_v1and apply trigger updates. - For full upstream details, see feistel_cipher v1.1.0 UPGRADE.md.