Skip to main content

secure-keystore — Python

Encrypted secrets at rest, with no service to run. One dependency: cryptography.

PyPI Python licence format

pip install secure-keystore
import os
from secure_keystore import Keystore

store = Keystore()
store.secure("./keys.env", os.environ["USER_KEY"], "production")
api_key = store.get("api_key", os.environ["USER_KEY"], "production")

This is one of six implementations of the secure-keystore format. A store written here opens in the JavaScript, Go, .NET, PHP and Rust ones, and the other way round — which is not a claim but a test against a corpus the JavaScript implementation produced, and against cross-test.mjs, where all six write stores and all six read every one of them.


Two ways in

A store — a directory of encrypted values that one user key opens.

A token — one string in, one string out, nothing on disk.

from secure_keystore import encrypt_value, decrypt_value

token = encrypt_value("sk_live_abc123", user_key)
back = decrypt_value(token, user_key)

API

Keystore(keystore_dir=None, *, iterations=600_000, logger=None, create_dir=True)

Binds one keystore directory and one set of options, validated once here rather than on every call. The instance holds no secret: the user key is passed to each call that needs one and is never retained.

Argument Default
keystore_dir ./.keystore Where stores live.
iterations 600000 PBKDF2 rounds for the user key, on new and migrated stores. Recorded in the store. Does not apply to value keys, which use HKDF.
logger None Called with one line of progress. None means the package prints nothing — the default, and what a server wants.
create_dir True Create the keystore directory if absent.

Store operations

store.secure(input_file, user_key, store_name=None) -> SecureResult
store.get(key, user_key, store_name=None) -> str
store.get_all(user_key, store_name) -> dict[str, str]
store.set(key, value, user_key, store_name) -> SetResult
store.migrate(store_name, user_key) -> MigrateResult
store.delete_key(key, store_name) -> None
store.delete_store(store_name) -> None

get_all costs one PBKDF2 derivation however many keys the store holds — see SPEC.md §2.1.

Metadata — no user key needed

store.list_stores() -> list[str]
store.list_keys(store_name) -> list[str]
store.get_store_info(store_name) -> StoreInfo

Key names live in metadata in the clear, so a key name must not itself be a secret.

Bundles and tokens

store.export_store(store_name, backup_path=None) -> str
store.import_store(backup_path, store_name=None) -> str

encrypt_value(plaintext, user_key, *, iterations=None) -> str | None
decrypt_value(token, user_key) -> str
is_encrypted_value(value) -> bool
token_version(value) -> str | None

iterations lowers the cost for bulk work — a token pays one stretch per value, unlike a store. The count is recorded in the token, so decryption follows whatever was chosen.


Errors

Every failure is a KeystoreError with a stable .code:

from secure_keystore import ErrorCodes, KeystoreError

try:
    store.get("api_key", user_key, "production")
except KeystoreError as error:
    if error.code == ErrorCodes.BAD_USER_KEY:
        ...   # wrong password, tampered bytes, or a mismatched context
    if error.code == ErrorCodes.STORE_NOT_FOUND:
        ...   # not deployed yet

The codes are identical in all six implementations — SPEC.md §10. A wrong user key would otherwise surface as cryptography.exceptions.InvalidTag, which names neither the store nor the reason.


Notes specific to Python

AESGCM handles both nonce lengths. cryptography accepts a 16-byte nonce as well as the 12-byte one format 3.0 uses, so reading a format 2.0 store needs no special case here — unlike .NET, where it needs a whole GCM implementation. Do not let that make the 16-byte IV look fine; see ROADMAP.md §1.1.

AESGCM.encrypt appends the tag to the ciphertext; the format stores them as separate hex fields, so the last 16 bytes are split off.

Type hints throughout, and a py.typed marker, so mypy sees the API.

Ordering. entries is a list and stays one; dict preserves insertion order in the versions this supports, so a parsed input file keeps the document's key order.


The CLI

secure-keystore get api_key "$USER_KEY" production
python -m secure_keystore --help          # the same thing, without the entry point

Same verbs, flags, error codes and --json output as every other implementation — see the root README.


Development

pip install -e ".[dev]"
pytest                      # includes the shared corpus
python/
├── src/secure_keystore/
│   ├── __init__.py       the public surface, and ALGORITHMS
│   ├── _format.py        everything the format fixes
│   ├── errors.py         KeystoreError and its codes
│   ├── _crypto.py        PBKDF2, HKDF, AES-GCM — the only place a cipher is built
│   ├── token.py          the stateless skv2 token
│   ├── input_file.py     the three input formats, testable without a file
│   ├── bundle.py         the .skb bundle
│   ├── keystore.py       the store itself
│   └── cli.py
└── tests/

Further reading

Licence

MIT © Tech Style Ltd

Metadata

Release files for secure-keystore 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for secure-keystore 3.0.0
File Size Uploaded
secure_keystore-3.0.0.tar.gz 32.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for secure-keystore 3.0.0
File Interpreter ABI Platform
secure_keystore-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 64.4 kB

Release files / secure_keystore-3.0.0.tar.gz

Download URL secure_keystore-3.0.0.tar.gz
Size 32.7 kB
Tags Source
SHA-256 checksum
How to use checksums
18b1a9e0d73783257db564013169ab51efc2456b3edbcc378f21e918153ba119
BLAKE2b-256 checksum
How to use checksums
8189455818413e4aab1cd81a56987a1e9d1f3da9a7a9c4ca6df57e9978f358b6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release files / secure_keystore-3.0.0-py3-none-any.whl

Download URL secure_keystore-3.0.0-py3-none-any.whl
Size 31.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
95aa7db16ecff53df2da7519f9ff8f5c4c6fda6ea4e6552b283fba5cba98f3f1
BLAKE2b-256 checksum
How to use checksums
8400f74da131e0e72a39c6836d6b8513e133fd4d7f19671d57bee8e1a4158ee5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release history Release notifications | RSS feed

This release

3.0.0 This release

2 release 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