Skip to main content

sqlalchemy-encrypted-field

Encrypted column types for SQLAlchemy. Secrets are ciphertext at rest, plaintext only where you ask for it.

from sqlalchemy_encrypted_field import EncryptedTextField, configure

configure(os.environ["SQLALCHEMY_ENCRYPTED_FIELD_KEYS"])


class Server(Base):
    __tablename__ = "servers"

    id: Mapped[int] = mapped_column(primary_key=True)
    token = EncryptedTextField(default="")


session.add(Server(token="sk-live-..."))
session.commit()

server.token  # Secret(<redacted>)
server.token.decrypt()  # "sk-live-..."

What it does differently

Every envelope is bound to its column. The associated data is the owning table.column, so ciphertext copied from one column into another fails to decrypt. Without this, anyone with write access to the database can move a low-value token into a high-value column, or one tenant's row into another's, without ever touching a key.

Key rotation has no flag day. The key setting is a comma-separated list. The first key encrypts, every key decrypts. Rotating means prepending a new key and dropping the old one once nothing needs it, with no downtime and no bulk re-encryption.

Loading a row never decrypts it. Reads hand back a Secret, not a string. Decryption happens only where decrypt() is called, and repr() is redacted, so a logged row or a stack trace cannot leak the value.

Failures are named. A dropped key raises SecretDecryptError, whose message points at the key configuration rather than a bare crypto traceback. Using a column before configure() raises NotConfiguredError instead of silently storing plaintext.

Install

pip install sqlalchemy-encrypted-field

Requires Python 3.11+, SQLAlchemy 2.0+. Tested on 3.11 through 3.14.

Fields

Field Assign Read back
EncryptedTextField() str Secret, use .decrypt() -> str
EncryptedBytesField() bytes SecretBytes, use .decrypt() -> bytes
EncryptedJsonField() dict SecretsMapping, a lazily-decrypting dict

All three store LargeBinary and are NOT NULL. Empty values ("", b"") are stored as empty bytes with no envelope, so a missing secret costs no crypto and needs no key.

SecretsMapping tracks in-place writes, so row.data["token"] = "new" marks the column dirty and persists on flush. Reach for it when a secret is genuinely a mapping; a single value belongs in an EncryptedTextField.

Assigning a loaded Secret to a different column re-encrypts it under that column's associated data, so moving a value between columns works.

Keys

Generate one:

python -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())"

configure() takes that string, or a comma-separated list of them. It also takes a callable returning the string, which is consulted on every operation. Use the callable form when keys come from settings that can be reloaded at runtime.

configure(lambda: settings.secrets_key)

validate_keys(value) is exposed separately so the same rules can run inside your own settings validation and fail at startup. A malformed or absent key is always an error, never a fallback to plaintext. Validation errors never echo the key material, because they end up in logs.

Envelope

version[1] || salt[16] || nonce[12] || ciphertext[n] || tag[16]

AES-256-GCM. The salt derives a per-envelope key with HKDF-SHA256 over the configured master key, so the master never reaches a cipher directly and nonce reuse across envelopes is not a concern. The table.column string is the associated data.

configure(..., info=b"...") overrides the HKDF info string. The default suits new projects. Set it only to keep reading envelopes written by an existing store that used a different one, and never change it once data exists, because it is part of the key derivation.

What it is not

This encrypts values so that a database dump, a backup, or a replica is not a pile of readable credentials. It does not protect against an attacker who can read your process memory or your key.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sqlalchemy_encrypted_field-1.0.1.tar.gz (10.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlalchemy_encrypted_field-1.0.1-py3-none-any.whl (9.7 kB view details)

Uploaded Python 3

File details

Details for the file sqlalchemy_encrypted_field-1.0.1.tar.gz.

File metadata

File hashes

Hashes for sqlalchemy_encrypted_field-1.0.1.tar.gz
Algorithm Hash digest
SHA256 7c7d637e3ebbbc9dc5a64a6d0489bcfab0201cbc35c1f49f17a9c3c0b8c08eb8
MD5 541cfa18a94dd9cbfff2a17f379d2b12
BLAKE2b-256 b9f7693fe27f4f734a0d6a846b7fe014d9dbd635a4be2217ea10fdd60adf167c

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqlalchemy_encrypted_field-1.0.1.tar.gz:

Publisher: release.yml on czpython/sqlalchemy-encrypted-field

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sqlalchemy_encrypted_field-1.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for sqlalchemy_encrypted_field-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 1c360b924f1df60b5446e4413880e06675d3a70454dfe58c9a1483c4ee9f3007
MD5 1183f969d08d0e1a63ac587786ef591c
BLAKE2b-256 20f3b95ca836cc5c0149a0de5d1beac822347b85ffc0528f006a4fd47e94280d

See more details on using hashes here.

Provenance

The following attestation bundles were made for sqlalchemy_encrypted_field-1.0.1-py3-none-any.whl:

Publisher: release.yml on czpython/sqlalchemy-encrypted-field

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page