PII Encryption¶
AMSDAL provides built-in field-level encryption for Personally Identifiable Information (PII). Mark sensitive fields with the PIIStr type, and AMSDAL automatically encrypts values before writing to the database and decrypts them on demand when reading.
Data is encrypted using AES-256-GCM with envelope encryption backed by AWS KMS — the same approach used by AWS services themselves.
from amsdal.models import Model, PIIStr
from pydantic import Field
class Customer(Model):
name: str
email: PIIStr = Field(title='email')
phone: PIIStr | None = Field(default=None, title='phone')
With this definition:
emailandphoneare stored encrypted in the database- When reloaded from the database, they come back as encrypted ciphertext by default
- To get plaintext values back, explicitly request decryption via QuerySet or REST API
Tip
PIIStr, EncryptedStr, and get_pii_fields are re-exported from amsdal.models for convenience. The fully-qualified path amsdal_models.classes.fields.pii also works.
Live instance vs database read¶
The live Customer object you constructed never holds ciphertext: encryption produces ciphertext only in the data sent to the database, the in-memory instance is left untouched. So immediately after save(), accessing customer.email still returns the original plaintext on that same instance.
Encrypted ciphertext is what comes back from a fresh QuerySet read — i.e. when the row is loaded from the database. Those values come back wrapped in EncryptedStr, a str subclass that masks itself in repr() / str() / format() to prevent accidental log leakage. Use the .ciphertext property to access the raw encrypted bytes if you need them.
How It Works¶
┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Your App │ ──── │ AMSDAL │ ──── │ PII Cryptor │
│ (Model) │ │ (Framework) │ │ (Service) │
└─────────────┘ └──────────────────┘ └──────────────────┘
│
┌───────┴────────┐
│ AWS KMS │
└────────────────┘
- You define model fields using
PIIStr - On save, AMSDAL detects PII fields and sends their values to the PII Cryptor service for encryption before writing to the database
- On read, encrypted values are returned as-is unless you explicitly request decryption
- The PII Cryptor service uses envelope encryption: AWS KMS generates a data encryption key, the service encrypts values with AES-256-GCM, and only the encrypted key is stored alongside the ciphertext
This means plaintext PII never exists in your database — even if the database is compromised, the data remains protected.
Defining PII Fields¶
Use PIIStr as the field type for any string field containing sensitive data:
from amsdal.models import Model, PIIStr
from pydantic import Field
class User(Model):
username: str
email: PIIStr = Field(title='email')
ssn: PIIStr | None = Field(default=None, title='ssn')
PIIStr is a regular str with metadata that tells AMSDAL to encrypt/decrypt it. You can use it anywhere you'd use str — required fields, optional fields, with defaults.
Detecting PII Fields Programmatically¶
from amsdal.models import get_pii_fields
fields = get_pii_fields(User)
# ['email', 'ssn']
Encryption and Decryption¶
Encryption — Automatic on Save¶
PII fields are encrypted automatically when saving objects. No extra code needed:
user = User(username='alice', email='alice@example.com', ssn='123-45-6789')
await User.objects.bulk_acreate([user])
# email and ssn are encrypted before hitting the database
In the database, the values look like:
#01#<encrypted_key>:<iv>:<ciphertext>:<tag>#10#
Decryption — Opt-in on Read¶
By default, PII fields return encrypted ciphertext. To get plaintext values, chain .decrypt_pii() on the QuerySet:
# Encrypted values (default)
users = await User.objects.aexecute()
print(users[0].email) # #01#abc123:def456:ghi789:jkl000#10#
# Decrypted values
users = await User.objects.decrypt_pii().aexecute()
print(users[0].email) # alice@example.com
This is intentional — decryption requires a call to the PII Cryptor service and ultimately to AWS KMS, so it only happens when you explicitly need the plaintext.
Note
decrypt_pii() is a regular chainable queryset method — it works in both sync and async mode (qs.decrypt_pii().aexecute() is valid). There is no adecrypt_pii async sibling; the chaining happens before the terminal aexecute() call where the actual decryption batch fires.
Querying by PII Fields¶
Random-IV ciphertext is non-deterministic — encrypting the same plaintext twice yields two different ciphertexts. As a result, filtering, excluding, or ordering by a PIIStr field never matches anything. AMSDAL emits a WARNING in the logger when it detects such a query:
User.objects.filter(email='alice@example.com') # logs warning, returns empty
User.objects.exclude(email='alice@example.com') # logs warning
User.objects.order_by('email') # logs warning
The walker also follows FK paths — filter(profile__email=...) is detected the same way.
If you need to look up records by PII, store a separate non-PII identifier (e.g. a hashed lookup key in a regular str field) and filter by that.
PII fields cannot be primary keys or unique constraints¶
A PIIStr field declared in __primary_key__ or any UniqueConstraint raises PIIInvalidFieldUsageError at class definition time. Random-IV ciphertext makes both checks vacuous — a fresh ciphertext on every save means the same value would never identify the same row, and uniqueness can never be detected.
REST API¶
All object endpoints support the decrypt_pii query parameter:
GET /api/objects/?class_name=User&decrypt_pii=true
GET /api/objects/{address}/?decrypt_pii=true
POST /api/objects/?decrypt_pii=true
PATCH /api/objects/{address}/?decrypt_pii=true
When decrypt_pii=true, the response includes decrypted PII field values. When omitted or false, encrypted ciphertext is returned.
Configuration¶
AMSDAL Cloud¶
When deploying to AMSDAL Cloud, PII encryption works out of the box — no configuration needed. The AMSDAL_PII_CRYPTOR_BASE_URL and AMSDAL_PII_CRYPTOR_CLIENT_ID are set automatically during deployment.
By default, all deployed applications use shared encryption keys. If you need a dedicated KMS key for your application (e.g. for compliance or data isolation requirements), contact us and we'll provision one for you.
Self-Hosted / Local Development¶
When running outside AMSDAL Cloud, the cryptor is configured via environment variables:
| Variable | Description | Default |
|---|---|---|
AMSDAL_PII_CRYPTO_SERVICE |
Import path to the cryptor class to use | amsdal.services.pii_cryptor.PIICryptorService |
AMSDAL_PII_CRYPTOR_BASE_URL |
URL of the PII Cryptor service (required by the real cryptor) | "" |
AMSDAL_PII_CRYPTOR_CLIENT_ID |
Client identifier for per-client KMS keys (real cryptor) | "" |
The default PIICryptorService will refuse to start if AMSDAL_PII_CRYPTOR_BASE_URL is empty — failing fast prevents the worst failure mode, where data looks encrypted but isn't.
For local development, point AMSDAL_PII_CRYPTO_SERVICE at the bundled fake:
# .env
AMSDAL_PII_CRYPTO_SERVICE=amsdal.services.fake_crypto.FakeCryptoService
FakeCryptoService wraps values with #01#...#10# markers without real encryption and logs a single WARNING banner on startup so it can never be mistaken for real encryption in logs. Never use FakeCryptoService in production.
amsdal new generates a project-level .env selecting the fake cryptor by default (and .env is gitignored), so a freshly created app runs locally out of the box.
Production deployments use the default PIICryptorService. When deploying to AMSDAL Cloud, AMSDAL_PII_CRYPTOR_BASE_URL is provisioned automatically — nothing is required from the user. For self-hosted deployments, set it explicitly to your PII Cryptor endpoint.
Custom cryptor¶
Any class implementing encrypt/decrypt/aencrypt/adecrypt (and an argless initialize() that validates its own configuration) can be plugged in via AMSDAL_PII_CRYPTO_SERVICE.
Security Architecture¶
Envelope Encryption¶
The PII Cryptor service implements envelope encryption:
- Encrypt: AWS KMS generates a unique Data Encryption Key (DEK) per request. Values are encrypted with AES-256-GCM using this DEK. The DEK itself is encrypted by KMS and stored alongside the ciphertext.
- Decrypt: The encrypted DEK is sent to KMS for decryption, then used to decrypt the values locally.
This means AWS KMS never sees your actual data — only the encryption keys.
AES-256-GCM¶
Each value is encrypted with:
- AES-256 — 256-bit symmetric encryption
- GCM mode — authenticated encryption that detects tampering
- Random IV — unique initialization vector per value, even within the same request
Key Management¶
- Encryption keys are managed by AWS KMS — you never handle raw key material
- Supports per-client KMS keys for multi-tenant isolation via the
X-CLIENT-IDheader - AWS KMS handles automatic key rotation (every 365 days), retaining old key versions for decryption
What's Protected¶
| Layer | Protection |
|---|---|
| Database | PII stored as encrypted ciphertext |
| Application | Decryption only happens on explicit request |
| Transport | HTTPS between service and KMS |
| Key storage | Keys managed by AWS KMS, never stored in plaintext |