Skip to content

Field encryption

Encrypt sensitive fields like SSN or credit cards using AWS KMS.

Key features

  • Per-field encryption with KMS
  • 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."""

from pydynox import Model, ModelConfig
from pydynox.attributes import EncryptedAttribute, StringAttribute


class User(Model):
    model_config = ModelConfig(table="users")

    pk = StringAttribute(hash_key=True)
    email = StringAttribute()
    ssn = EncryptedAttribute(key_id="alias/my-app-key")


# Create a user with sensitive data
user = User(
    pk="USER#123",
    email="john@example.com",
    ssn="123-45-6789",
)
user.save()

# The SSN is encrypted in DynamoDB as "ENC:base64data..."
# When you read it back, it's decrypted automatically
loaded = User.get(pk="USER#123")
print(loaded.ssn)  # "123-45-6789"

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(hash_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(hash_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(hash_key=True)
    ssn = EncryptedAttribute(key_id="alias/my-app-key")

If you try to decrypt in WriteOnly mode, you get an EncryptionError. 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(hash_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

  1. On save, the attribute calls KMS Encrypt with your plaintext
  2. KMS returns ciphertext encrypted with your key
  3. The ciphertext is base64-encoded and stored with ENC: prefix
  4. On read, the attribute detects the prefix and calls KMS Decrypt
  5. KMS returns the original plaintext

All encryption happens in Rust for speed. The KMS client is created lazily on first use.

Storage format

Encrypted values are stored as:

ENC:<base64-encoded-ciphertext>

Values without the ENC: prefix are treated as plaintext. This means you can add encryption to existing fields - old unencrypted values still work.

Limitations

  • AWS credentials from environment - Uses the default credential chain (env vars, IAM role, etc.). You cannot pass credentials directly.
  • Region from environment - Uses AWS_REGION or AWS_DEFAULT_REGION env var by default. You can override with the region parameter.
  • 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:Encrypt",
        "kms:Decrypt"
    ],
    "Resource": "arn:aws:kms:us-east-1:123456789:key/your-key-id"
}

For WriteOnly mode, you only need kms:Encrypt. For ReadOnly, only kms:Decrypt.

Error handling

Encryption errors raise EncryptionError:

from pydynox.exceptions import EncryptionError

try:
    user.save()
except EncryptionError 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