Skip to main content

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

config = {
    "ServiceName": "my-service",
    "ProductID":   "my-product",
    "Metastore":   "memory",
    "KMS":         "static",
    "StaticMasterKeyHex": "22" * 32,
}

with asherah.SessionFactory(config) as factory:
    with factory.get_session("user-42") as session:
        ct = session.encrypt_text("secret")
        pt = session.decrypt_text(ct)
        assert pt == "secret"

SessionFactory(config) and SessionFactory.from_config(config) construct from an explicit config dict. SessionFactory() and SessionFactory.from_env() read 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 with loop.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:

  • NoneTypeError from PyO3 type conversion before any native call.
  • Empty str ("") and empty bytes (b"") are valid plaintexts. encrypt_string / encrypt_bytes produce a real DataRowRecord envelope; decrypt_string / decrypt_bytes return exactly "" or b"".

Ciphertext to decrypt:

  • NoneTypeError.
  • Empty str / bytes → exception from JSON parse (not valid DataRowRecord).

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.pyi and asherah/__init__.py and 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(config) Construct from an explicit config dict.
SessionFactory.from_env() Same as SessionFactory() — provided for SDK parity.
SessionFactory.from_config(config) Construct from an explicit config dict.
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 strbytes.
session.decrypt_text(drr) DRR JSON strstr.
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

asherah-0.5.55.tar.gz (303.5 kB view details)

Uploaded Source

Built Distributions

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

asherah-0.5.55-cp38-abi3-win_arm64.whl (7.1 MB view details)

Uploaded CPython 3.8+Windows ARM64

asherah-0.5.55-cp38-abi3-win_amd64.whl (7.6 MB view details)

Uploaded CPython 3.8+Windows x86-64

asherah-0.5.55-cp38-abi3-musllinux_1_2_x86_64.whl (12.1 MB view details)

Uploaded CPython 3.8+musllinux: musl 1.2+ x86-64

asherah-0.5.55-cp38-abi3-musllinux_1_2_aarch64.whl (12.3 MB view details)

Uploaded CPython 3.8+musllinux: musl 1.2+ ARM64

asherah-0.5.55-cp38-abi3-manylinux_2_28_x86_64.whl (11.2 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.28+ x86-64

asherah-0.5.55-cp38-abi3-manylinux_2_28_aarch64.whl (11.8 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.28+ ARM64

asherah-0.5.55-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl (17.2 MB view details)

Uploaded CPython 3.8+macOS 10.12+ universal2 (ARM64, x86-64)macOS 10.12+ x86-64macOS 11.0+ ARM64

File details

Details for the file asherah-0.5.55.tar.gz.

File metadata

  • Download URL: asherah-0.5.55.tar.gz
  • Upload date:
  • Size: 303.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for asherah-0.5.55.tar.gz
Algorithm Hash digest
SHA256 69bfc2e4d93bef69c8cbef50c97a31ddee583783f80c07fa8783092a36adf01a
MD5 e94555d5dc55ab47d51a9bb8d0a5108e
BLAKE2b-256 b83cc868d6cc383eda12dcbad59d585078134d0de3f9ab45c6230684dc201c58

See more details on using hashes here.

File details

Details for the file asherah-0.5.55-cp38-abi3-win_arm64.whl.

File metadata

  • Download URL: asherah-0.5.55-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

Hashes for asherah-0.5.55-cp38-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 8b36f9348cb156b6d694b1565fb73b4898a617e5c99c4368f59493a327789f18
MD5 cc1cdba70faac0164dad3cc064bd56bf
BLAKE2b-256 3b12856ba33dfc7ff74f0af7f05946de57d888741460dc3b9eb95e4ae768f449

See more details on using hashes here.

File details

Details for the file asherah-0.5.55-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: asherah-0.5.55-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

Hashes for asherah-0.5.55-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 6204d269618f8989f873727a4ffa8b668a19aad883c78b12d774323a46f16f8b
MD5 afa82be7b996ccf8f1603df3e3a4b643
BLAKE2b-256 aaab8f4500a9eaa7e8d0661cccf1795650d9f7b9c88009cad59e23a38569f62f

See more details on using hashes here.

File details

Details for the file asherah-0.5.55-cp38-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for asherah-0.5.55-cp38-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 08943b73a77aab799c16188fba6c64e3a72332c58a7c6bb2cf42ce3e523b8252
MD5 73af988e2068ce3b45545a4e63bb7708
BLAKE2b-256 e9d98db35507dec78ca96a909c68404c17ab2ebb6f224594d780a8d14d8bb44e

See more details on using hashes here.

File details

Details for the file asherah-0.5.55-cp38-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for asherah-0.5.55-cp38-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 9c1b6a951718ef67bcd835e611f058e5f2b320df6eeee7895a3af2469a003c79
MD5 e086185df381ba3f6f7ab94b2dde0e7b
BLAKE2b-256 4f12b587814a417ece0a2876d214592b7a5d0d3841aa66f3d86ba399e4c2e45f

See more details on using hashes here.

File details

Details for the file asherah-0.5.55-cp38-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for asherah-0.5.55-cp38-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 342d1e57b18b9c1f97b2c0c6f7c0418712de54ed7569b183467b256355153477
MD5 41b51241e85a00aac5f6429eff9bbbcb
BLAKE2b-256 8f8e97af4edb968d1c2f2d3de23023cba6130079f94b2c6d62f81c0689c95137

See more details on using hashes here.

File details

Details for the file asherah-0.5.55-cp38-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for asherah-0.5.55-cp38-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 90855c4457532ded56bdf9ed42a6c02d20c3127a1cf1b816ddb703bc3e22a7db
MD5 0c37d3f50bbb19172cd859d8d2c111a2
BLAKE2b-256 3942ac142c791fa23367c77fb425c87a17d2c220fad5fd2dbc3dff204125e7e9

See more details on using hashes here.

File details

Details for the file asherah-0.5.55-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl.

File metadata

File hashes

Hashes for asherah-0.5.55-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
Algorithm Hash digest
SHA256 08b8b76c8bea4771cf99cd3de9ded61314ceda04dad75e89a5f38a6e7273e8b3
MD5 a1cfde0ef7da7f09d7d4f5b71c6edde7
BLAKE2b-256 d9420503f286ad87777a2fe35b80dd96c2770f24a45744960dfc915ec4ccd853

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page