keystonecrypto
Note on badge values: the
Tests,Coverage, andConformancebadges reflect the test suite at v0.1.0 (101 test functions across 12 files; coverage ≥ 95% on Layers 1–3; 10 BIP-39 + 11 BIP-32 + 3 RFC 8032
- 1 RFC 9106 vector). Run
pytest --cov=keystonecryptolocally for the actual current measurement.
Non-custodial hierarchical deterministic (HD) wallet primitives for Python.
Generate BIP-39 seeds, derive addresses across Bitcoin, Ethereum, and Solana, encrypt keystores with Argon2id + AES-256-GCM, and sign messages with secp256k1 ECDSA, BIP-340 Schnorr, or RFC 8032 Ed25519. Pluggable chain adapters ship as optional extras.
The security-critical core (key management + signing) is the default
install. Optional [btc], [eth], [sol] extras add per-chain
transaction building and broadcast. No chain extras means no network
calls — the core is offline by default.
Install
pip install keystonecrypto # core only (key mgmt + signing)
pip install keystonecrypto[btc] # + Bitcoin (PSBT, Electrum)
pip install keystonecrypto[eth] # + Ethereum (tx + JSON-RPC)
pip install keystonecrypto[sol] # + Solana (tx + RPC)
pip install keystonecrypto[network] # + httpx for chain RPC clients
Python 3.11+ required. Tested on 3.12 and 3.13. Python 3.14 is not yet supported because some C-extension dependencies do not publish prebuilt wheels for it.
Five-minute tour
1. Generate a new mnemonic and persist it
from keystonecrypto import Keystore, Mnemonic
# 12-word phrase from 128 bits of OS entropy.
# Use strength=256 for a 24-word phrase.
mnemonic = Mnemonic.generate()
print(mnemonic.phrase) # NEVER log this in production code
# Encrypt it under a user passphrase using Argon2id + AES-256-GCM.
keystore = Keystore.create(mnemonic, "correct horse battery staple")
keystore.save("wallet.keystore") # ~200 bytes, opaque binary
2. Open the keystore, unlock it, derive keys
from keystonecrypto import Keystore, DerivationPath
with Keystore.open("wallet.keystore") as ctx:
with ctx.unlock("correct horse battery staple") as wallet:
# The wallet holds the seed in memory. close() zeroizes it.
# BIP-44 first receive address for Bitcoin (m/44'/0'/0'/0/0).
path = DerivationPath.parse("m/44'/0'/0'/0/0")
account = wallet.derive_account(path) # default: ECDSA over secp256k1
print(account.public_key_hex())
# Ethereum uses coin type 60, not 0.
eth_path = DerivationPath.parse("m/44'/60'/0'/0/0")
eth_account = wallet.derive_account(eth_path)
# (Address derivation itself is in the [eth] extra.)
3. Sign and verify messages
from keystonecrypto import (
Keystore, Mnemonic, DerivationPath,
Secp256kSigner, Secp256kSchnorrSigner, Ed25519Signer,
)
with Keystore.open("wallet.keystore") as ctx, \
ctx.unlock("correct horse battery staple") as wallet:
path = DerivationPath.parse("m/44'/0'/0'/0/0")
# ECDSA over secp256k1 (Bitcoin, Ethereum) — 64-byte compact signature.
sig = wallet.sign_message(path, b"hello world", scheme="ecdsa-secp256k1")
assert sig.verify(b"hello world", wallet.derive_account(path).public_key)
# BIP-340 Schnorr (Taproot key-path spends).
sig = wallet.sign_message(path, b"hello world", scheme="schnorr-secp256k1")
assert sig.verify(b"hello world", wallet.derive_account(path).public_key)
# RFC 8032 Ed25519 (Solana).
sol_path = DerivationPath.parse("m/44'/501'/0'/0'")
sig = wallet.sign_message(sol_path, b"hello world", scheme="ed25519")
assert sig.verify(b"hello world", wallet.derive_account(sol_path).public_key)
4. Sign without a keystore (lower-level API)
from keystonecrypto import (
Mnemonic, ExtendedPrivateKey, derive_from_seed,
Secp256kSigner, Ed25519Signer,
)
from keystonecrypto.secret_bytes import SecretBytes
seed_bytes = Mnemonic.from_phrase(
"abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon about"
).seed(passphrase="") # 64-byte SecretBytes
xprv = derive_from_seed(seed_bytes, DerivationPath.parse("m/44'/0'/0'/0/0"))
signer = Secp256kSigner(xprv.private_key)
sig = signer.sign(b"raw message")
assert sig.verify(b"raw message", signer.public_key_bytes())
BIP-39 passphrase ("25th word")
A BIP-39 passphrase is appended to the mnemonic before seed derivation. It is not recoverable — losing the passphrase means losing the wallet, even with the correct mnemonic.
from keystonecrypto import Mnemonic
m = Mnemonic.from_phrase(
"abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon about"
)
# Same mnemonic + different passphrase = different wallet.
seed_no_pp = m.seed(passphrase="") # default
seed_with_pp = m.seed(passphrase="my-secret-25th")
assert bytes(seed_no_pp) != bytes(seed_with_pp)
Mnemonic.generate() returns a phrase; the passphrase is supplied at
seed-derivation time and stored in the keystore envelope's
has_passphrase flag.
Keystore envelope (binary format)
The keystore is an opaque binary blob, ~200 bytes for a 32-byte seed. Layout (big-endian, version 1 = "KST1"):
Header (66 bytes):
magic 4 "KST1"
version 1 0x01
kdf_id 1 0x01 = Argon2id
kdf_params 32 (time_cost | memory_cost | parallelism |
hash_len | version | reserved)
salt 16
nonce 12
Body (variable):
ciphertext N (AES-256-GCM with 16-byte appended tag)
Decryption binds the ciphertext to the (version, kdf_id) via GCM
associated data, so downgrades and cross-context replay fail closed.
See SECURITY.md for the threat model and a list of
known adversarial behaviors that fail correctly.
Argon2id profiles
from keystonecrypto import Keystore
from keystonecrypto.kdf import (
INTERACTIVE_PROFILE, HIGH_SECURITY_PROFILE, Argon2Params,
)
# Default: OWASP 2024 minimum for interactive use.
ks = Keystore.create(mnemonic, "pw", params=INTERACTIVE_PROFILE)
# Cold storage / high-value wallets: 2x memory, +1 time.
ks = Keystore.create(mnemonic, "pw", params=HIGH_SECURITY_PROFILE)
# Custom profile — must meet OWASP minimums (~19 MiB, time_cost ≥ 2,
# parallelism ≥ 1). Tests bypass this with Argon2Params.for_testing(...).
Chain adapters (optional extras)
The [btc], [eth], and [sol] extras each ship in v0.2.0. They
provide per-chain address derivation, transaction construction, and
broadcast clients. Until then, the core covers key generation and
signing — you bring your own chain logic.
Example after v0.2.0:
# pip install "keystonecrypto[btc]" (planned for v0.2.0)
from keystonecrypto.btc.address import derive_p2wpkh_from_path
from keystonecrypto.btc.psbt import PSBT
from keystonecrypto.btc.electrum import Electrum
# Native SegWit address at m/84'/0'/0'/0/0.
addr = derive_p2wpkh_from_path(wallet, "m/84'/0'/0'/0/0")
# Sign a PSBT and broadcast via Electrum.
psbt = PSBT.from_base64(raw_psbt)
signed = psbt.sign(wallet)
txid = await Electrum(client).broadcast(signed.to_base64())
Status note: chain adapters (
keystonecrypto[btc],[eth],[sol]) ship in v0.2.0 and are not yet part of v0.1.0. The function names above show the intended API and may change before release. The plan atdocs/superpowers/plans/2026-09-20-keystonecrypto.mdtasks T14–T22 covers what will land.
API reference
Top-level exports (from keystonecrypto import ...):
| Name | Purpose |
|---|---|
Keystore |
Encrypted mnemonic on disk; create, save, open |
KeystoreContext |
Context manager; .unlock(password) -> Wallet |
Wallet |
Unlocked wallet; .derive_account(path), .sign_message(...), .close() |
Mnemonic |
BIP-39 phrase; .generate(), .from_phrase(), .from_entropy(), .seed() |
DerivationPath |
BIP-32 path; parse("m/44'/0'/0'/0/0") |
ExtendedPrivateKey |
BIP-32 xprv; .derive(i, hardened=...), .to_base58() |
derive_from_seed |
One-shot derivation from seed + path |
SecretBytes |
Zeroizing wrapper; random(length), .zeroize(), .hex() |
Secp256kSigner |
secp256k1 ECDSA, 64-byte compact signature |
Secp256kSchnorrSigner |
BIP-340 Schnorr (Taproot key-path) |
Ed25519Signer |
RFC 8032 Ed25519 (Solana) |
STRENGTH_BITS, generate_entropy |
BIP-39 entropy sizes |
Security
Read SECURITY.md before using this library in
production. Highlights:
- The default install has zero network capability. A lint guard
prevents network libraries (
httpx,requests,socket, etc.) from being imported in core code. - All sensitive memory lives in
SecretBytes, which zeroizes on context-manager exit and on garbage collection. - All hash and compare operations use constant-time primitives.
- Public keystore construction enforces OWASP 2024 Argon2id minimums
(
Argon2Params.for_testing(...)is the only escape hatch, used by the RFC 9106 conformance test). - Conformance: BIP-39 (10 official trezor vectors), BIP-32 (11 official spec vectors), RFC 8032 (3 Ed25519 vectors), RFC 9106 (Argon2id § 5).
This library is not a hosted service, not a custodial wallet, not a UI. It is a building block. The user is responsible for securing the host process, the passphrase, and the keystore file.
Glossary
Every term used in this README and the public API.
Cryptography
| Term | Definition |
|---|---|
| AEAD (Authenticated Encryption with Associated Data) | Encryption mode that produces both ciphertext and an authentication tag, binding the ciphertext to a context string so tampering or cross-context replay is detectable. We use AES-256-GCM. |
| AES-256-GCM | The Advanced Encryption Standard with 256-bit keys, in Galois/Counter Mode. Symmetric encryption (same key for encrypt and decrypt) producing a 12-byte nonce, ciphertext, and 16-byte authentication tag. |
| Argon2id | A memory-hard password hashing function. The recommended KDF for password-based key derivation because it resists both GPU and side-channel attacks. Winner of the 2015 Password Hashing Competition. |
| Authentication tag | The 16-byte value AES-GCM appends to the ciphertext. Decryption verifies the tag matches; mismatch raises KeystoreDecryptionError. |
| Constant-time | An operation whose execution time does not depend on secret data, preventing timing side-channel attacks. We use hmac.compare_digest for byte comparison and audited libsecp256k1 for scalar arithmetic. |
| CSPRNG | A Cryptographically Secure Pseudo-Random Number Generator. We use secrets.token_bytes, which draws from the OS entropy source (/dev/urandom on Unix, BCryptGenRandom on Windows). |
| ECDSA | Elliptic Curve Digital Signature Algorithm. We use it over secp256k1 for Bitcoin and Ethereum. Produces a 64-byte compact signature (r, s). |
| Ed25519 | A signature scheme using the Edwards-curve Curve25519. RFC 8032 standard. Used by Solana. |
| KDF (Key Derivation Function) | A slow, memory-hard function that turns a password into a cryptographic key. |
| libsecp256k1 | The reference C implementation of secp256k1 elliptic-curve operations, written by Bitcoin Core contributors and audited. Used via the coincurve Python wrapper. |
| Schnorr signature (BIP-340) | A signature scheme over secp256k1 with provable security under standard assumptions and native multi-sig aggregation. Taproot (BIP-341) uses BIP-340 Schnorr for key-path spends. |
| secp256k1 | The elliptic curve used by Bitcoin and Ethereum. Parameters: y² = x³ + 7 over the finite field of order 2^256 - 2^32 - 977. |
| Zeroize | To overwrite memory containing a secret with zeros, ideally before it can be read by an attacker. SecretBytes.zeroize() overwrites the bytearray in place. |
BIP standards
| Term | Definition |
|---|---|
| BIP-32 | Hierarchical Deterministic Wallets. Defines how to derive a tree of keypairs from a single seed using HMAC-SHA512 and secp256k1 scalar addition. |
| BIP-39 | Mnemonic code for generating deterministic keys. Defines the wordlist, the entropy-to-mnemonic mapping, and the PBKDF2-HMAC-SHA512 seed derivation (2048 iterations). |
| BIP-44 | Multi-account hierarchy for HD wallets. Defines the 5-level path structure m / purpose' / coin_type' / account' / change / address_index. |
| BIP-49 | Derivation scheme for P2SH-wrapped SegWit (3... addresses). Path: m/49'/coin_type'/account'/change/address_index. |
| BIP-84 | Derivation scheme for native SegWit v0 (bc1q... addresses). Path: m/84'/coin_type'/account'/change/address_index. |
| BIP-86 | Derivation scheme for single-key Taproot (bc1p... addresses). Path: m/86'/coin_type'/account'/change/address_index. |
| Chain code | The 32-byte auxiliary value in a BIP-32 extended key, used to derive children. |
| Extended key (xprv/xpub) | A BIP-32 serialized private (xprv) or public (xpub) key, containing the key, chain code, depth, parent fingerprint, and child number. 78 bytes, base58check-encoded. |
| Hardened derivation | A BIP-32 derivation mode in which the parent private key is required. Indices >= 2^31. Indicated by the apostrophe in m/44'. |
| Master key | The BIP-32 key derived directly from a seed via HMAC-SHA512. The root of the derivation tree. |
| Mnemonic | A BIP-39 phrase: 12, 15, 18, 21, or 24 words from a 2048-word list. Encodes 128, 160, 192, 224, or 256 bits of entropy plus a checksum. |
| Mnemonic passphrase (25th word) | An optional user-supplied string combined with the mnemonic to produce a different seed. Not stored anywhere; losing it = losing the wallet. |
| Non-hardened derivation | A BIP-32 derivation mode using only the parent public key. Indices < 2^31. Indicated by the lack of apostrophe in m/0. |
| Seed | The 512-bit output of BIP-39 seed derivation (PBKDF2-HMAC-SHA512, 2048 iterations). Used as the input to BIP-32 master key generation. |
| Wordlist | The 2048 English words defined by BIP-39, in alphabetical order. Index 0 = "abandon", index 2047 = "zoo". |
Wallet concepts
| Term | Definition |
|---|---|
| Account (BIP-44) | The third level of a BIP-44 path (account'), allowing multiple wallets under the same seed (e.g. personal vs. business funds). |
| Address | A public, human-shareable identifier derived from a public key. Different chains use different address formats (Base58 for legacy BTC, Bech32 for SegWit, hex with EIP-55 checksum for ETH, base58 for SOL). |
| Air-gapped wallet | A wallet whose keys never touch an internet-connected device. Generate keys offline, sign transactions on the offline device, broadcast from an online device. |
| BIP-44 path | A 5-level derivation path: m / purpose' / coin_type' / account' / change / address_index. change is 0 for receive, 1 for change outputs. |
| Change output | In Bitcoin, the "leftover" amount from a transaction that's sent back to the spender. Uses change = 1 in the derivation path. |
| Coin type | The second level of a BIP-44 path, identifying the blockchain. 0 = Bitcoin, 60 = Ethereum, 501 = Solana. |
| Custodial wallet | A wallet where a third party holds the keys on the user's behalf. This library is NOT custodial. |
| Derivation path | The string used to derive a specific key from a master seed. Examples: m, m/44'/0'/0'/0/0, m/0'/1. |
| HD wallet (Hierarchical Deterministic) | A wallet where a single seed generates a tree of keypairs. Contrast with non-deterministic (random) wallets. |
| Keystore | In this library, an encrypted BIP-39 mnemonic persisted to disk. ~200 bytes, KST1 binary format, Argon2id + AES-256-GCM. |
| Non-custodial wallet | A wallet where the user holds the keys. This library is non-custodial. |
| PSBT (Partially Signed Bitcoin Transaction, BIP-174) | A binary interchange format for partially-signed Bitcoin transactions, used to pass transactions between wallets and signers. |
| Purpose | The first level of a BIP-44 path, identifying the derivation scheme. 44 = BIP-44, 49 = BIP-49, 84 = BIP-84, 86 = BIP-86. |
| Receive address | An address given to others to send funds to. Uses change = 0 in the BIP-44 path. Should be used exactly once for privacy. |
| Seed phrase | Synonym for mnemonic. A BIP-39 phrase. |
| Sighash | The hash a signer commits to when signing a transaction input. Different per-input types: BIP-143 (SegWit v0), BIP-341 (Taproot). |
| Signer | In this library, an object with .sign(message) -> Signature and .public_key_bytes() -> bytes. Three implementations: secp256k1 ECDSA, BIP-340 Schnorr, Ed25519. |
| Wallet (this library) | An in-memory object holding the seed, returned by KeystoreContext.unlock(...). Closed via context-manager exit or .close(). |
| xprv | See Extended key. |
Encryption envelope (this library)
| Term | Definition |
|---|---|
| KST1 | The magic bytes marking a keystonecrypto v1 keystore file. Any blob not starting with these four bytes is rejected. |
| kdf_id | The 1-byte identifier for the key derivation function used to wrap the seed. 0x01 = Argon2id. Future versions may add scrypt, PBKDF2, etc. |
| magic | Four bytes at the start of a binary file identifying its format. We use KST1. |
| OWASP minimums | The Password Storage Cheat Sheet's Argon2id recommendations as of 2024: memory_cost ≥ 19 MiB, time_cost ≥ 2, parallelism ≥ 1. We enforce these for public Argon2Params(...) construction. |
| Salt | Random bytes mixed into the KDF to prevent rainbow-table attacks. We use 16 bytes from the OS CSPRNG. |
| Version byte | The 1-byte field after KST1 indicating the envelope version. We currently support version 0x01 only. Unknown versions are rejected with KeystoreVersionError. |
Threat model
| Term | Definition |
|---|---|
| AEAD failure | The exception raised when AES-GCM authentication fails. We use KeystoreDecryptionError. Never leaks which byte was wrong. |
| Downgrade attack | An attempt to use a weaker version of a primitive (e.g. lower Argon2id cost) than the current default. Defended by binding the ciphertext to the (version, kdf_id) via GCM associated data. |
| Fail closed | A failure mode where any unexpected condition results in the secure default (deny access, refuse to decrypt, reject the request). Every keystore error path fails closed. |
| Harvest now, decrypt later | An adversary records today's encrypted traffic or public keys, then decrypts them later when cryptographically relevant quantum computers exist. HD wallets are narrow targets: seeds live in keystores, not on-chain. |
| Side-channel attack | An attack that uses physical effects of computation (timing, power consumption, cache patterns) rather than direct cryptanalysis. Mitigated via constant-time primitives and audited libraries. |
License
Apache-2.0. See LICENSE.
Metadata
Release files for keystonecrypto 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| keystonecrypto-0.1.0.tar.gz | 103.9 kB | Without provenance | Details for keystonecrypto-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| keystonecrypto-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for keystonecrypto-0.1.0-py3-none-any.whl |
Total release size: 142.8 kB
Release files / keystonecrypto-0.1.0.tar.gz
| Download URL | keystonecrypto-0.1.0.tar.gz |
|---|---|
| Size | 103.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ed27b7a71680e057b382dfb7905c45419a9ccba5e045cd3791a2d6457d2d402
|
|
BLAKE2b-256 checksum How to use checksums |
4e44d26b6b19f91daecc946a8fce57407ccb24b9a14ed3ff0a25b8574907b2d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.1
|
Release files / keystonecrypto-0.1.0-py3-none-any.whl
| Download URL | keystonecrypto-0.1.0-py3-none-any.whl |
|---|---|
| Size | 38.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6b20d409c530ca8be016e196d9bedf004968327d5e317a3316682d11a91e2f5c
|
|
BLAKE2b-256 checksum How to use checksums |
9d13747410e9420ddadae79534a6168f9b873974c8ddda4be27862e922db32ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.1
|
Security for keystonecrypto 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.