Asherah application-layer encryption for Python with automatic key rotation, powered by the native Rust implementation.
Project description
asherah
Python bindings for the Asherah envelope encryption and key rotation library.
Native Rust implementation via PyO3/maturin. Prebuilt wheels are published to PyPI for Linux (x86_64 and aarch64, both glibc and musl), macOS (x86_64 and arm64), and Windows (x86_64 and arm64).
Installation
pip install asherah
Requires Python ≥ 3.8.
Documentation
Task-oriented walkthroughs under docs/:
| Guide | When to read |
|---|---|
| Getting started | First-time install through round-trip encrypt/decrypt. |
| Framework integration | FastAPI, Flask, Django, AWS Lambda, Celery. |
| AWS production setup | KMS keys, DynamoDB, IAM policy, region routing. |
| Testing | pytest fixtures, Testcontainers, mocking patterns, asyncio test patterns. |
| Troubleshooting | Common errors with what to check first. |
Choosing an API style
Two API styles are exposed; both are fully supported and produce the same wire format. New code should prefer the Factory / Session API.
| Style | When to use |
|---|---|
Static / module-level (asherah.setup, asherah.encrypt_bytes, …) |
Drop-in compatibility with the canonical godaddy/asherah-python package. Simplest call surface. Singleton lifecycle (setup() once, shutdown() once). |
Factory / Session (asherah.SessionFactory, factory.get_session(...)) |
Recommended for new code. Explicit lifecycle, no hidden singleton, multi-tenant isolation is obvious in code. Context-manager friendly. |
A complete runnable example exercising both styles plus async, log hook, and
metrics hook is in samples/python/sample.py.
Quick start (static API)
import os
import asherah
os.environ["STATIC_MASTER_KEY_HEX"] = "22" * 32 # testing only
asherah.setup({
"ServiceName": "my-service",
"ProductID": "my-product",
"Metastore": "memory", # testing only — use "rdbms" or "dynamodb" in production
"KMS": "static", # testing only — use "aws" in production
})
ct = asherah.encrypt_string("user-42", "secret")
pt = asherah.decrypt_string("user-42", ct)
assert pt == "secret"
asherah.shutdown()
Quick start (factory / session API)
import asherah
with asherah.SessionFactory() as factory:
with factory.get_session("user-42") as session:
ct = session.encrypt_text("secret")
pt = session.decrypt_text(ct)
assert pt == "secret"
SessionFactory reads its config from environment variables. Set them with
asherah.setenv({...}) or via os.environ before constructing the factory.
Async API
There are two flavors of async to choose from depending on your call pattern:
-
Module-level async (
encrypt_string_async,decrypt_string_async,setup_async,shutdown_async) — wraps the sync calls withloop.run_in_executor. Lowest setup, but the sync work runs on the default thread pool executor. -
Session-level async (
session.encrypt_bytes_async,session.decrypt_bytes_async) — true async PyO3 coroutines that run on the Rust tokio runtime. The asyncio event loop is not blocked, and there is no thread pool overhead.
import asyncio
import asherah
async def main():
# Module-level
await asherah.setup_async({...})
ct = await asherah.encrypt_string_async("user-42", "secret")
pt = await asherah.decrypt_string_async("user-42", ct)
await asherah.shutdown_async()
# Session-level (true async)
with asherah.SessionFactory() as factory:
session = factory.get_session("user-42")
ct = await session.encrypt_bytes_async(b"secret")
pt = await session.decrypt_bytes_async(ct)
asyncio.run(main())
Observability hooks
Log hook
Receive every log event from the Rust core (encrypt/decrypt path, metastore drivers, KMS clients).
def on_log(event):
# event = {"level": "trace"|"debug"|"info"|"warn"|"error",
# "message": str, "target": str}
if event["level"] in ("warn", "error"):
print(f"[asherah {event['level']}] {event['message']}")
asherah.set_log_hook(on_log)
# later, to deregister:
asherah.set_log_hook(None)
The callback may fire from any thread (Rust tokio worker threads, DB driver threads). PyO3 acquires the GIL before invoking the callback, so the callback runs single-threaded from Python's perspective.
Metrics hook
Receive timing events for encrypt/decrypt/store/load and counter events for cache hit/miss/stale.
def on_metric(event):
if event["type"] in ("encrypt", "decrypt", "store", "load"):
# event = {"type": ..., "duration_ns": int}
my_histogram.observe(event["type"], event["duration_ns"] / 1e6)
else:
# event = {"type": "cache_hit"|"cache_miss"|"cache_stale", "name": str}
my_counter.inc(result=event["type"], cache=event["name"])
asherah.set_metrics_hook(on_metric)
# later:
asherah.set_metrics_hook(None)
Metrics collection is enabled automatically when a hook is installed and disabled when cleared.
Input contract
Partition ID (None, ""): always rejected as programming errors
with TypeError (None) or ValueError/Exception ("partition id
cannot be empty"). No row is ever written to the metastore under a
degenerate partition ID.
Plaintext to encrypt:
None→TypeErrorfrom PyO3 type conversion before any native call.- Empty
str("") and emptybytes(b"") are valid plaintexts.encrypt_string/encrypt_bytesproduce a realDataRowRecordenvelope;decrypt_string/decrypt_bytesreturn exactly""orb"".
Ciphertext to decrypt:
None→TypeError.- Empty
str/bytes→ exception from JSON parse (not validDataRowRecord).
Do not short-circuit empty plaintext encryption in caller code — empty data is real data, encrypting it produces a genuine envelope, and skipping encryption leaks the fact that the value was empty. See docs/input-contract.md for the full rationale.
Configuration
setup() accepts a dict (or any JSON-serializable object) using
PascalCase keys to match the canonical Go/Java/.NET API:
| Key | Type | Required | Description |
|---|---|---|---|
ServiceName |
str | yes | Service identifier for the key hierarchy. |
ProductID |
str | yes | Product identifier for the key hierarchy. |
Metastore |
str | yes | "memory", "rdbms", or "dynamodb". "memory" is testing-only. |
KMS |
str | "static" (default; testing) or "aws". |
|
ConnectionString |
str | SQL connection string for rdbms. |
|
SQLMetastoreDBType |
str | "mysql" or "postgres" (paired with Metastore: "rdbms"). |
|
EnableSessionCaching |
bool | Cache Session objects by partition ID. Default True. |
|
SessionCacheMaxSize |
int | Max cached sessions. Default 1000. | |
SessionCacheDuration |
int | Session cache TTL in seconds. | |
RegionMap |
dict[str,str] | AWS KMS multi-region key-ARN map. | |
PreferredRegion |
str | Preferred region from RegionMap. |
|
AwsProfileName |
str | AWS shared-credentials profile name for KMS, DynamoDB, and Secrets Manager clients. | |
EnableRegionSuffix |
bool | Append AWS region suffix to key IDs. | |
ExpireAfter |
int | Intermediate-key expiration in seconds. Default 90 days. | |
CheckInterval |
int | Revoke-check interval in seconds. Default 60 minutes. | |
DynamoDBEndpoint |
str | DynamoDB endpoint URL (for local DynamoDB). | |
DynamoDBRegion |
str | AWS region for DynamoDB. | |
DynamoDBTableName |
str | DynamoDB table name. Default EncryptionKey. |
|
ReplicaReadConsistency |
str | DynamoDB consistency. | |
Verbose |
bool | Emit verbose log events (use a log hook to consume). | |
EnableCanaries |
bool | Enable in-memory canary buffers around plaintexts. |
Both PascalCase and snake_case keys are accepted; PascalCase is canonical.
Environment variables
| Variable | Effect |
|---|---|
STATIC_MASTER_KEY_HEX |
64 hex chars (32 bytes) for static KMS. Testing only. |
SERVICE_NAME / PRODUCT_ID / Metastore / KMS |
Read by SessionFactory() (no-config constructor). |
AWS KMS example
asherah.setup({
"ServiceName": "payments-api",
"ProductID": "acme-corp",
"Metastore": "rdbms",
"ConnectionString": "mysql://user:pass@host:3306/asherah",
"SQLMetastoreDBType": "mysql",
"KMS": "aws",
"RegionMap": {"us-west-2": "arn:aws:kms:us-west-2:000:key/abc"},
"PreferredRegion": "us-west-2",
"EnableSessionCaching": True,
"SessionCacheMaxSize": 1000,
})
Performance
Native Rust implementation. Typical latencies on Apple M4 Max (in-memory metastore, session caching enabled, 64-byte payload):
| Operation | Sync | Async (session-level, true async) |
|---|---|---|
| Encrypt | ~1 µs | ~37 µs |
| Decrypt | ~1.2 µs | ~37 µs |
Async overhead is from the asyncio event loop dispatch + GIL handoff. Use sync for CPU-bound batches; use async when you need non-blocking behavior in an asyncio application.
API Reference
Full docstrings live in
asherah/_asherah.pyiandasherah/__init__.pyand surface in your IDE on hover. The tables below summarize each API; the type stubs are the source of truth.
Static / module-level API (legacy compatibility)
Lifecycle
| Function | Description |
|---|---|
setup(config: dict) |
Initialize the global instance. Raises if already configured. |
setup_async(config: dict) |
Async wrapper. Returns a coroutine. |
shutdown() |
Tear down the global instance. Idempotent. |
shutdown_async() |
Async wrapper. |
get_setup_status() -> bool |
True iff setup() has been called and shutdown() has not. |
setenv(env: dict) |
Apply env vars before setup(). Values may be None to delete. |
version() -> str |
Package version string. |
Encrypt / decrypt
| Function | Param 1 | Param 2 | Returns |
|---|---|---|---|
encrypt_bytes(partition_id, data) |
str (non-empty) |
bytes (empty OK) |
str (DRR JSON) |
encrypt_string(partition_id, text) |
str |
str (empty OK) |
str (DRR JSON) |
decrypt_bytes(partition_id, drr) |
str |
str |
bytes |
decrypt_string(partition_id, drr) |
str |
str |
str |
encrypt_bytes_async(partition_id, data) |
str |
bytes |
Awaitable[str] |
decrypt_bytes_async(partition_id, drr) |
str |
str or bytes |
Awaitable[bytes] |
encrypt_string_async(partition_id, text) |
str |
str |
Awaitable[str] |
decrypt_string_async(partition_id, drr) |
str |
str |
Awaitable[str] |
Hooks
| Function | Description |
|---|---|
set_log_hook(callback) |
Register a (event_dict) -> None log callback. Pass None to deregister. |
set_metrics_hook(callback) |
Register a (event_dict) -> None metrics callback. Pass None to deregister. |
Factory / Session API (recommended)
class SessionFactory
| Member | Description |
|---|---|
SessionFactory() |
Construct from environment variables. |
SessionFactory.from_env() |
Same as SessionFactory() — provided for SDK parity. |
factory.get_session(partition_id) |
Get a per-partition Session. Raises on null/empty partition. |
factory.close() |
Release native resources. |
with SessionFactory() as factory: |
Context manager — close() runs on exit. |
class Session
| Member | Description |
|---|---|
session.encrypt_bytes(data) |
bytes → DRR JSON str. Empty bytes is valid. |
session.encrypt_text(text) |
str → DRR JSON str. Empty string is valid. |
session.decrypt_bytes(drr) |
DRR JSON str → bytes. |
session.decrypt_text(drr) |
DRR JSON str → str. |
session.encrypt_bytes_async(data) |
Awaitable[str] — true async on tokio. |
session.decrypt_bytes_async(drr) |
Awaitable[bytes] — true async on tokio. |
session.close() |
Release native resources. |
with session as ...: |
Context manager — close() runs on exit. |
Event dict shapes
LogEvent = {
"level": "trace" | "debug" | "info" | "warn" | "error",
"message": str,
"target": str,
}
# Metrics event for timing measurements:
TimingEvent = {
"type": "encrypt" | "decrypt" | "store" | "load",
"duration_ns": int,
}
# Metrics event for cache lifecycle:
CacheEvent = {
"type": "cache_hit" | "cache_miss" | "cache_stale",
"name": str, # cache name, e.g. "session", "intermediate-key"
}
Cross-language compatibility
Wire-format compatible with all other Asherah implementations:
- canonical
godaddy/asherah(Go core via cobhan) - canonical
godaddy/asherah-csharp - canonical
godaddy/asherah-java - this repo's other bindings: Node, .NET, Java, Ruby, Go
A DataRowRecord written by any of these can be decrypted by any other,
provided they share the same metastore and KMS configuration.
License
Licensed under the Apache License, Version 2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
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 asherah-0.5.45.tar.gz.
File metadata
- Download URL: asherah-0.5.45.tar.gz
- Upload date:
- Size: 311.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14947a5e3c3d25476062e80760d0a08cded9ad9b1e2ce8d759fac80d28d1a124
|
|
| MD5 |
91d2a7c33d45da0d1275cad02767e91b
|
|
| BLAKE2b-256 |
6164ea644a7c41f87fbd73bcf32029ce1ddd34f0d459c74d20dd6fb1e20fb074
|
File details
Details for the file asherah-0.5.45-cp38-abi3-win_arm64.whl.
File metadata
- Download URL: asherah-0.5.45-cp38-abi3-win_arm64.whl
- Upload date:
- Size: 7.1 MB
- Tags: CPython 3.8+, Windows ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88aee7a385bcf1e1ba6b20ac1662308f8701dae345db30101cbe5b58433c3765
|
|
| MD5 |
6d79f11d2b50411042bf83648cdf8316
|
|
| BLAKE2b-256 |
9d4698ba6c382e852f943408aff57d9e46105e6d54273c170c77b6f60ed84577
|
File details
Details for the file asherah-0.5.45-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: asherah-0.5.45-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 7.6 MB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41e38a35e9583b6176c478cec7fdbe1e348819fae60cf4b68c158314868f2058
|
|
| MD5 |
d94d922ccf65dbc0eaa53774475b75e2
|
|
| BLAKE2b-256 |
115fdd5dbe8c708f21facdaede0928091fd135b7c1f871b32393aa3ca8f66b77
|
File details
Details for the file asherah-0.5.45-cp38-abi3-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: asherah-0.5.45-cp38-abi3-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 12.1 MB
- Tags: CPython 3.8+, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dae70b224b6ff6c773b4592fbe9388d1e13c258c4f9377cbda4b0a81a1065ec5
|
|
| MD5 |
2026768401fc50b29be39b42d6665e97
|
|
| BLAKE2b-256 |
db1db8eb5296d1065664dda4586fdd8eb7fe934edbe5f6a5b40384ccb9a95df6
|
File details
Details for the file asherah-0.5.45-cp38-abi3-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: asherah-0.5.45-cp38-abi3-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 12.2 MB
- Tags: CPython 3.8+, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
624317def1890ed528d65d2d02b7087af3ad86b93a876c68ddedd8fad7fac59f
|
|
| MD5 |
91228b3261e9697eeae969ba979a3d8a
|
|
| BLAKE2b-256 |
c9a8838bfb1901351e785b54ea29afa8ef285b0eb8a4f2f50124ed64c147e7fd
|
File details
Details for the file asherah-0.5.45-cp38-abi3-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: asherah-0.5.45-cp38-abi3-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 11.2 MB
- Tags: CPython 3.8+, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9240e9eeef61d3237969d80b81f8eb6fa75815c5acb9a954839146f95195940
|
|
| MD5 |
21e9bafffceab0b10e4d98df4e585527
|
|
| BLAKE2b-256 |
e3e26c2d5d795bd89c194993caa6afa3ed20116a8da22750e46fe8c3420ba7a6
|
File details
Details for the file asherah-0.5.45-cp38-abi3-manylinux_2_28_aarch64.whl.
File metadata
- Download URL: asherah-0.5.45-cp38-abi3-manylinux_2_28_aarch64.whl
- Upload date:
- Size: 11.7 MB
- Tags: CPython 3.8+, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7395ce3769667ed89a0126dc1051b78b45b8c3d09d03da73f01423c2bce6103a
|
|
| MD5 |
94a28a73828b2eeaebde9da35cac875e
|
|
| BLAKE2b-256 |
e04b19c18349346d117822ddafaa5aabb03d88ea4906f28c193a844a781a0ba5
|
File details
Details for the file asherah-0.5.45-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl.
File metadata
- Download URL: asherah-0.5.45-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
- Upload date:
- Size: 17.1 MB
- Tags: CPython 3.8+, macOS 10.12+ universal2 (ARM64, x86-64), macOS 10.12+ x86-64, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
abd7a77747836e5fd1d3f76ba87945d89d9a38976713d975dcda2e72014b3a9a
|
|
| MD5 |
e6d5e3c68ccfcb5d4c37af9f9169e59a
|
|
| BLAKE2b-256 |
19a46a7413cef1de5cdaa8f15799dd0fa0e9458beb70ccb19b6103654ba4b704
|