Crypto utilities, encrypted SQLAlchemy fields, and experimental secret resolvers.
Project description
gemstone_utils
gemstone_utils provides a small, stable core of cryptographic helpers, transparent AES‑GCM encrypted SQLAlchemy fields, and a minimal experimental secrets resolver suitable for Pydantic’s BeforeValidator. It is designed for applications that need reversible secret storage with minimal plaintext exposure and predictable operational behavior.
The package is licensed under the MPL‑2.0, allowing use in both open‑source and proprietary projects while keeping modifications to this library itself open.
Documentation
| Topic | Guide |
|---|---|
SQLAlchemy EncryptedString, init order, rotation |
docs/sqlalchemy.md |
Persisted keys (key_storage), two-level wire semantics, bootstrap |
docs/key-storage.md |
Experimental resolve_secret and backends |
docs/secrets-resolver.md |
| Curated public API (stable vs experimental) | docs/api.md |
Breaking changes and migration (including UUID key ids): RELEASE_NOTES.md. A data-migration outline for key ids: scripts/migrate_key_ids.py.
Features
🔐 Cryptography core
- Registered symmetric algorithms (
SYM_ALG_REGISTRY,SUPPORTED_SYM_ALGS) withencrypt_alg/decrypt_alg(optional per‑algorithm params; encrypt returns(ciphertext, updated_params)) recommended_data_alg()/RECOMMENDED_DATA_ALG— library default field algorithm id (avoids hardcoding in apps)generate_key_by_alg(alg)for DEK-sized random bytes from the registry- PBKDF2‑HMAC‑SHA256 key derivation (no extra dependencies)
- URL‑safe base64 encoding helpers
- Minimal, dependency‑light design
🧩 Encrypted fields
$A256GCM$<uuid>$base64(json)$base64(blob)encrypted‑field format (segment 2 is a canonical UUID string, typically UUIDv7 fromgemstone_utils.key_id.new_key_id())KeyContext(keyidasstr,key,alg) ingemstone_utils.typesencrypt_string()anddecrypt_string()helpers ingemstone_utils.encrypted_fields
🗄️ SQLAlchemy integration
EncryptedStringTypeDecorator- Transparent encryption on write
- Lazy decryption on read via
LazySecret - Prevents accidental double‑encryption
EncryptedString.set_current_keyctx()for the active write keyEncryptedString.set_keyctx_resolver()to mapkeyidfrom stored ciphertext to aKeyContexton read
🔐 Key management (key_mgmt)
derive_kek(passphrase, params)plus a pluggableregister_kdfregistry; params JSON is the source of truth for which algorithm runs.recommended_kdf_params()— library default for new KDF rows (today: PBKDF2‑HMAC‑SHA256 with strong iteration count and random salt).gemstone_utils.key_mgmt.kdf— documented contract (RecommendedKdfParamsFn,HasKdfRegistryName) and per‑algorithm modules (e.g.kdf.pbkdf2:NAME,pbkdf2_params,recommended_pbkdf2_params) when you pin a specific algorithm instead of the default.
🔑 SQL key storage (sqlalchemy.key_storage)
- Tables
gemstone_key_kdf(KEK slot: persisted KDF JSON,canary_wrapped, optionalapp_reencrypt_pending, timestamps) andgemstone_key_record(DEKs only: wrapped keys in the same five‑part wire format as encrypted columns, plusdata_alg,is_active,created_at,updated_at). Primary keys are UUID strings (String(36)until v0.9.0; see release notes). - The KEK canary lives on the
gemstone_key_kdfrow (canary_wrapped), not ingemstone_key_record. The segmentkeyidinside each wrapped DEK wire string is the KEK slot id (row ingemstone_key_kdf). The segment in applicationEncryptedStringciphertext is the DEK id (gemstone_key_record.key_id). - Bootstrap with
new_kdf_params()(alias ofrecommended_kdf_params) or your own params dict;saltmust always be stored in JSON for PBKDF2 rows. Useset_kdf_params,set_kek_canary, thenput_keyrecordfor the DEK. put_keyrecord()inserts DEK rows only (validatesdata_alg, maintains a single active DEK whenis_active=True, sets timestamps).make_keyctx_resolver()wiresEncryptedString.set_keyctx_resolver()toget_session()+ persisted rows +derive_kek/ unwrap;KeyContext.algcomes from the row’sdata_alg(field algorithm), not the wrap algorithm insidewrapped.rewrap_key_records()performsrotate_kek‑style batch re‑wrap inside a transaction you open withwith session.begin(): ...and bumpsupdated_aton touched rows.
Back up gemstone_key_kdf with the same care as gemstone_key_record: salt and iteration counts are required to recover KEKs from the vault passphrase.
🧪 Experimental secret resolver
Suitable for Pydantic BeforeValidator:
Supports:
env:— environment variables (cached + scrubbed)file:— read from filesystemsecret:— systemd / container secret directories- pluggable backend (
azexp:) enabled by explicitly importing the plugin module $A256GCM$keyid$base64(json)$base64(blob)encrypted values (requiressecrets_resolver.set_keyctx_resolver)
Not intended to be the final vault/meta‑manager.
Installation
pip install gemstone_utils
With Azure Key Vault support:
pip install 'gemstone_utils[azure]'
For running tests in a checkout:
pip install 'gemstone_utils[dev]'
Or from a source tarball:
pip install gemstone_utils-0.4.0.tar.gz
Quick Start
1. Derive a data key and wire EncryptedString
from gemstone_utils.key_mgmt import derive_kek, recommended_kdf_params
from gemstone_utils.key_mgmt.kdf.pbkdf2 import pbkdf2_params
from gemstone_utils.types import KeyContext
from gemstone_utils.sqlalchemy.encrypted_type import EncryptedString
from gemstone_utils.experimental.secrets_resolver import resolve_secret
passphrase = resolve_secret("env:APP_DK_PASSPHRASE")
salt = resolve_secret("env:APP_DK_SALT").encode("utf-8")
dk = derive_kek(passphrase, pbkdf2_params(salt))
# or: derive_kek(passphrase, recommended_kdf_params(salt))
from gemstone_utils.key_id import new_key_id
kid = new_key_id()
ctx = KeyContext(keyid=kid, key=dk)
EncryptedString.set_current_keyctx(ctx)
def resolve_enc_keyctx(keyid: str) -> KeyContext:
if keyid != ctx.keyid:
raise ValueError(f"unknown keyid {keyid}")
return ctx
EncryptedString.set_keyctx_resolver(resolve_enc_keyctx)
set_current_keyctx is used for new writes. set_keyctx_resolver is used on read to choose the correct KeyContext for each row’s embedded keyid (needed for rotation and multiple keys).
1b. Optional: persisted keys + EncryptedString resolver
import gemstone_utils.sqlalchemy.key_storage # registers ORM tables on GemstoneDB
from gemstone_utils.crypto import generate_key_by_alg, recommended_data_alg
from gemstone_utils.db import get_session, init_db
from gemstone_utils.key_id import new_key_id
from gemstone_utils.key_mgmt import derive_kek, init as key_mgmt_init, make_kek_check_record
from gemstone_utils.sqlalchemy.encrypted_type import EncryptedString
from gemstone_utils.sqlalchemy.key_storage import (
keyrecord_to_wire,
make_keyctx_resolver,
new_kdf_params,
put_keyrecord,
set_kdf_params,
set_kek_canary,
wire_wrap,
)
from gemstone_utils.types import KeyContext
init_db("sqlite:///./app.db")
key_mgmt_init("vault_passphrase", b"constant-canary-bytes", env_allowed=True)
passphrase = "human vault passphrase"
kdf = new_kdf_params()
kek = derive_kek(passphrase, kdf)
kek_id = new_key_id()
dek_id = new_key_id()
dek_material = generate_key_by_alg(recommended_data_alg())
dek = KeyContext(keyid=dek_id, key=dek_material)
with get_session() as session:
with session.begin():
set_kdf_params(session, kek_id, kdf)
set_kek_canary(
session,
kek_id,
keyrecord_to_wire(make_kek_check_record(kek), kek_id),
)
put_keyrecord(
session,
key_id=dek_id,
wrapped=wire_wrap(kek_id, kek, dek.key),
is_active=True,
)
EncryptedString.set_current_keyctx(dek)
EncryptedString.set_keyctx_resolver(
make_keyctx_resolver(load_passphrase=lambda: passphrase)
)
KEK rotation (new passphrase or new KEK under the same KDF row) uses rewrap_key_records inside with session.begin(): — see gemstone_utils.sqlalchemy.key_storage.
2. Use encrypted fields in SQLAlchemy models
from sqlalchemy import Column, Integer
from gemstone_utils.sqlalchemy.encrypted_type import EncryptedString
class OAuthToken(Base):
__tablename__ = "oauth_tokens"
id = Column(Integer, primary_key=True)
refresh_token = Column(EncryptedString, nullable=False)
3. Use the experimental secrets resolver with Pydantic
from pydantic import BaseModel, field_validator
from gemstone_utils.experimental.secrets_resolver import resolve_secret
class Config(BaseModel):
api_token: str
@field_validator("api_token", mode="before")
@classmethod
def load_secret(cls, v):
return resolve_secret(v)
Config example:
api_token = "secret:my_api_token"
Secret Resolver Backends
env:VAR
Reads from environment, caches, and scrubs the variable.
file:/path/to/file
Reads a file once and caches it.
secret:name
Searches:
$CREDENTIALS_DIRECTORY/name/run/secrets/name/var/run/secrets/name
azexp:https://vault.vault.azure.net/secrets/name
Fetches from Azure Key Vault. Enabled by importing
gemstone_utils.experimental.azexp_backend (or calling its enable()).
Install gemstone_utils[azure] and authenticate with DefaultAzureCredential.
Use azexp_backend.set_azexp_credential(...) to override credentials.
Encrypted field values ($A256GCM$…)
Values use the wire form $A256GCM$<uuid>$<base64(json)>$<base64(blob)> where <uuid> is a canonical UUID string (segment 2). URL-safe base64 of a JSON object for per-algorithm parameters (currently {} for A256GCM), then URL-safe base64 of the ciphertext blob. Automatically decrypted using secrets_resolver.set_keyctx_resolver (separate from EncryptedString.set_keyctx_resolver).
Experimental Components
The following modules are intentionally minimal and will not be part of the future vault/meta‑manager:
gemstone_utils.experimental.secrets_resolver
They exist to support early projects (GemstoneOps, Thaum, WebexCalling bridge) without constraining the design of the full resolver.
License
This project is licensed under the Mozilla Public License 2.0 (MPL‑2.0).
You may use it in proprietary applications, but modifications to this library itself must be published under the MPL.
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 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 gemstone_utils-0.4.0.tar.gz.
File metadata
- Download URL: gemstone_utils-0.4.0.tar.gz
- Upload date:
- Size: 33.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec6b918d905983f30ad35d15f31cc6599720a2ce21ef0ef02452ff984d2c6b53
|
|
| MD5 |
234586f29e6013670f0fb8c12bcbcae1
|
|
| BLAKE2b-256 |
bb74e3c3034a0a33c62d0d9fc768ccba4595e749e78c303f810f59b14e724ac5
|
Provenance
The following attestation bundles were made for gemstone_utils-0.4.0.tar.gz:
Publisher:
publish.yml on gemstone-software-dev/gemstone_utils
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gemstone_utils-0.4.0.tar.gz -
Subject digest:
ec6b918d905983f30ad35d15f31cc6599720a2ce21ef0ef02452ff984d2c6b53 - Sigstore transparency entry: 1500930469
- Sigstore integration time:
-
Permalink:
gemstone-software-dev/gemstone_utils@56479308d5af3e418ea1c6bcd972f7bfd5a0fb9b -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/gemstone-software-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@56479308d5af3e418ea1c6bcd972f7bfd5a0fb9b -
Trigger Event:
release
-
Statement type:
File details
Details for the file gemstone_utils-0.4.0-py3-none-any.whl.
File metadata
- Download URL: gemstone_utils-0.4.0-py3-none-any.whl
- Upload date:
- Size: 34.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ffb0f47ccec7b56149b3c4000b82a17288241a882287aa5eaa9e67caa730e8ac
|
|
| MD5 |
486334f3803f97f4d2b718cca3b5be50
|
|
| BLAKE2b-256 |
e740f9398fad2e40a062e761ce02364815faa3e5bbd11dc955d149dde4e5cd06
|
Provenance
The following attestation bundles were made for gemstone_utils-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on gemstone-software-dev/gemstone_utils
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gemstone_utils-0.4.0-py3-none-any.whl -
Subject digest:
ffb0f47ccec7b56149b3c4000b82a17288241a882287aa5eaa9e67caa730e8ac - Sigstore transparency entry: 1500930516
- Sigstore integration time:
-
Permalink:
gemstone-software-dev/gemstone_utils@56479308d5af3e418ea1c6bcd972f7bfd5a0fb9b -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/gemstone-software-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@56479308d5af3e418ea1c6bcd972f7bfd5a0fb9b -
Trigger Event:
release
-
Statement type: