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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlalchemy_encrypted_field-1.0.1.tar.gz.
File metadata
- Download URL: sqlalchemy_encrypted_field-1.0.1.tar.gz
- Upload date:
- Size: 10.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7c7d637e3ebbbc9dc5a64a6d0489bcfab0201cbc35c1f49f17a9c3c0b8c08eb8
|
|
| MD5 |
541cfa18a94dd9cbfff2a17f379d2b12
|
|
| BLAKE2b-256 |
b9f7693fe27f4f734a0d6a846b7fe014d9dbd635a4be2217ea10fdd60adf167c
|
Provenance
The following attestation bundles were made for sqlalchemy_encrypted_field-1.0.1.tar.gz:
Publisher:
release.yml on czpython/sqlalchemy-encrypted-field
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlalchemy_encrypted_field-1.0.1.tar.gz -
Subject digest:
7c7d637e3ebbbc9dc5a64a6d0489bcfab0201cbc35c1f49f17a9c3c0b8c08eb8 - Sigstore transparency entry: 2688106208
- Sigstore integration time:
-
Permalink:
czpython/sqlalchemy-encrypted-field@948a56cf8143f60099b4300b411a290a60d685c0 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/czpython
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@948a56cf8143f60099b4300b411a290a60d685c0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file sqlalchemy_encrypted_field-1.0.1-py3-none-any.whl.
File metadata
- Download URL: sqlalchemy_encrypted_field-1.0.1-py3-none-any.whl
- Upload date:
- Size: 9.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c360b924f1df60b5446e4413880e06675d3a70454dfe58c9a1483c4ee9f3007
|
|
| MD5 |
1183f969d08d0e1a63ac587786ef591c
|
|
| BLAKE2b-256 |
20f3b95ca836cc5c0149a0de5d1beac822347b85ffc0528f006a4fd47e94280d
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sqlalchemy_encrypted_field-1.0.1-py3-none-any.whl -
Subject digest:
1c360b924f1df60b5446e4413880e06675d3a70454dfe58c9a1483c4ee9f3007 - Sigstore transparency entry: 2688106254
- Sigstore integration time:
-
Permalink:
czpython/sqlalchemy-encrypted-field@948a56cf8143f60099b4300b411a290a60d685c0 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/czpython
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@948a56cf8143f60099b4300b411a290a60d685c0 -
Trigger Event:
release
-
Statement type: