Field encryption
Protect sensitive data like SSN, credit cards, or API keys at rest. pydynox encrypts fields before saving to DynamoDB and decrypts them when reading back. Uses AWS KMS for key management.
Key features
- Per-field encryption with KMS envelope encryption
- No size limit (works with fields up to 400KB)
- Three modes: ReadWrite, WriteOnly, ReadOnly
- Encryption context for extra security
- Automatic encrypt on save, decrypt on load
Getting started
Basic usage
Add EncryptedAttribute to fields that need encryption:
"""Basic field encryption example."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import EncryptedAttribute, StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
email = StringAttribute()
ssn = EncryptedAttribute(key_id="alias/my-app-key")
async def main():
# Create a user with sensitive data
user = User(
pk="USER#ENC",
sk="PROFILE",
email="john@example.com",
ssn="123-45-6789",
)
await user.save()
# The SSN is encrypted in DynamoDB as "ENC:base64data..."
# When you read it back, it's decrypted automatically
loaded = await User.get(pk="USER#ENC", sk="PROFILE")
print(loaded.ssn) # "123-45-6789"
asyncio.run(main())
The field is encrypted before saving to DynamoDB. When you read it back, it's decrypted automatically. In DynamoDB, the value looks like ENC:base64data....
Encryption modes
Not all services need both encrypt and decrypt. A service that only writes data shouldn't be able to read it back. Use modes to control this:
| Mode | Can encrypt | Can decrypt | Use case |
|---|---|---|---|
ReadWrite |
✓ | ✓ | Full access (default) |
WriteOnly |
✓ | ✗ (returns encrypted) | Ingest services |
ReadOnly |
✗ (returns plaintext) | ✓ | Report services |
Import EncryptionMode from pydynox.attributes:
"""Encryption modes example."""
from pydynox import Model, ModelConfig
from pydynox.attributes import EncryptedAttribute, EncryptionMode, StringAttribute
# Write-only service: can encrypt, cannot decrypt
class IngestService(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
ssn = EncryptedAttribute(
key_id="alias/my-app-key",
mode=EncryptionMode.WriteOnly,
)
# Read-only service: can decrypt, cannot encrypt
class ReportService(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
ssn = EncryptedAttribute(
key_id="alias/my-app-key",
mode=EncryptionMode.ReadOnly,
)
# Full access (default): can encrypt and decrypt
class AdminService(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
ssn = EncryptedAttribute(key_id="alias/my-app-key")
If you try to decrypt in WriteOnly mode, you get an EncryptionException. Same for encrypting in ReadOnly mode.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key_id |
str | Required | KMS key ID, ARN, or alias |
mode |
EncryptionMode | ReadWrite | Controls encrypt/decrypt access |
region |
str | None | AWS region (uses env default) |
context |
dict | None | Encryption context for extra security |
Advanced
Encryption context
KMS supports encryption context - extra key-value pairs that must match on decrypt. If someone tries to decrypt with a different context, it fails.
"""Encryption context example."""
from pydynox import Model, ModelConfig
from pydynox.attributes import EncryptedAttribute, StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
ssn = EncryptedAttribute(
key_id="alias/my-app-key",
context={"tenant": "acme-corp", "purpose": "pii"},
)
# The context is passed to KMS on encrypt/decrypt.
# If the context doesn't match, decryption fails.
# This adds an extra layer of security.
This is useful for:
- Multi-tenant apps - Include tenant ID in context
- Audit - Context is logged in CloudTrail
- Extra validation - Ensure data is decrypted in the right context
How it works
pydynox uses envelope encryption for field-level encryption:
Encrypt:
- Call
KMS:GenerateDataKeyonce to get a plaintext key + encrypted key - Use the plaintext key to AES-256-GCM encrypt locally (fast, in Rust)
- Pack the encrypted key + encrypted data together
- Base64 encode and add
ENC:prefix
Decrypt:
- Decode base64 and unpack the envelope
- Call
KMS:Decrypton the encrypted key to get plaintext key - Use the plaintext key to AES-256-GCM decrypt locally
This approach has two big advantages over direct KMS Encrypt/Decrypt:
- No 4KB limit - KMS Encrypt only accepts 4KB, but DynamoDB fields can be 400KB
- Fewer KMS calls - One call per operation instead of one per field
Storage format
Encrypted values are stored as:
The envelope contains: - Version byte (for future compatibility) - Encrypted data key length (2 bytes) - Encrypted data key (from KMS) - Encrypted data (AES-256-GCM with random nonce)
Values without the ENC: prefix are treated as plaintext. This means you can add encryption to existing fields - old unencrypted values still work.
Limitations
- Inherits credentials from DynamoDBClient - Uses the same AWS credentials configured in your
DynamoDBClient. No need to configure separately. - Strings only - Only encrypts string values. For other types, convert to string first.
- No key rotation - If you rotate your KMS key, old data still decrypts (KMS handles this), but you need to re-encrypt to use the new key.
IAM permissions
Your service needs these KMS permissions:
{
"Effect": "Allow",
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:123456789:key/your-key-id"
}
Note: We use kms:GenerateDataKey instead of kms:Encrypt. For ReadOnly mode, you only need kms:Decrypt.
Error handling
Encryption errors raise EncryptionException:
from pydynox.exceptions import EncryptionException
try:
user.save()
except EncryptionException as e:
print(f"Encryption failed: {e}")
Common errors:
| Error | Cause |
|---|---|
| KMS key not found | Wrong key ID or alias |
| Access denied | Missing IAM permissions |
| Cannot encrypt in ReadOnly mode | Wrong mode for operation |
| Cannot decrypt in WriteOnly mode | Wrong mode for operation |
| Decryption failed | Data corrupted or wrong key |
Next steps
- Size calculator - Check item sizes
- IAM permissions - KMS permissions
- Attributes - All attribute types