keystonecrypto 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ """keystonecrypto — non-custodial HD wallet primitives for Python.
2
+
3
+ This is the public API. Submodules are also importable directly for
4
+ advanced users; the names below are the recommended entry points.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __version__ = "0.1.0"
10
+
11
+ from keystonecrypto.derivation import DerivationPath, ExtendedPrivateKey, derive_from_seed
12
+ from keystonecrypto.entropy import STRENGTH_BITS, generate_entropy
13
+ from keystonecrypto.exceptions import KeystoneCryptoError
14
+ from keystonecrypto.keystore import Keystore, Wallet
15
+ from keystonecrypto.mnemonic import Mnemonic
16
+ from keystonecrypto.secret_bytes import SecretBytes
17
+ from keystonecrypto.signing import (
18
+ Ed25519Signer,
19
+ Secp256kSchnorrSigner,
20
+ Secp256kSigner,
21
+ )
22
+
23
+ __all__ = [
24
+ "__version__",
25
+ "KeystoneCryptoError",
26
+ "SecretBytes",
27
+ "Mnemonic",
28
+ "Keystore",
29
+ "Wallet",
30
+ "DerivationPath",
31
+ "ExtendedPrivateKey",
32
+ "derive_from_seed",
33
+ "STRENGTH_BITS",
34
+ "generate_entropy",
35
+ "Secp256kSigner",
36
+ "Secp256kSchnorrSigner",
37
+ "Ed25519Signer",
38
+ ]
keystonecrypto/aead.py ADDED
@@ -0,0 +1,55 @@
1
+ """AES-256-GCM authenticated encryption with associated data (AAD).
2
+
3
+ AAD binds the ciphertext to a context (e.g. keystore version, chain
4
+ identifier). Decryption with mismatched AAD fails the tag check, which
5
+ prevents cross-context ciphertext replay attacks.
6
+
7
+ Returns nonce separately from ciphertext so the caller can pack them
8
+ into whatever envelope they need. The tag is appended to the
9
+ ciphertext (cryptography's default).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+
16
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
17
+
18
+ from keystonecrypto.exceptions import KeystoreDecryptionError
19
+ from keystonecrypto.secret_bytes import SecretBytes
20
+
21
+ __all__ = ["encrypt", "decrypt", "NONCE_LEN", "KEY_LEN"]
22
+
23
+ NONCE_LEN: int = 12
24
+ KEY_LEN: int = 32
25
+
26
+
27
+ def _as_key(key: SecretBytes) -> bytes:
28
+ raw = bytes(key)
29
+ if len(raw) != KEY_LEN:
30
+ raise ValueError(f"AES-256-GCM key must be {KEY_LEN} bytes, got {len(raw)}")
31
+ return raw
32
+
33
+
34
+ def encrypt(key: SecretBytes, plaintext: bytes, aad: bytes = b"") -> tuple[bytes, bytes]:
35
+ """Encrypt `plaintext` under `key` with optional associated data.
36
+
37
+ Returns (nonce, ciphertext_plus_tag). The tag is appended to the
38
+ ciphertext per the `cryptography` library convention.
39
+ """
40
+ nonce = os.urandom(NONCE_LEN)
41
+ aes = AESGCM(_as_key(key))
42
+ ct = aes.encrypt(nonce, plaintext, aad)
43
+ return nonce, ct
44
+
45
+
46
+ def decrypt(key: SecretBytes, nonce: bytes, ciphertext: bytes, aad: bytes = b"") -> bytes:
47
+ """Decrypt `ciphertext` under `key`. Raises on tag mismatch or wrong key."""
48
+ if len(nonce) != NONCE_LEN:
49
+ raise ValueError(f"nonce must be {NONCE_LEN} bytes, got {len(nonce)}")
50
+ aes = AESGCM(_as_key(key))
51
+ try:
52
+ return aes.decrypt(nonce, ciphertext, aad)
53
+ except Exception as e:
54
+ # Wrap so callers get a typed exception.
55
+ raise KeystoreDecryptionError(f"AES-GCM decryption failed: {e}") from e
@@ -0,0 +1,218 @@
1
+ """BIP-32 hierarchical key derivation + BIP-44 path parsing.
2
+
3
+ Layer 2 module. The derivation primitive uses HMAC-SHA512 and secp256k1
4
+ scalar addition via `coincurve` (so the math is libsecp256k1, not a
5
+ hand-rolled implementation).
6
+
7
+ Path syntax:
8
+ m -> master
9
+ m/0 -> non-hardened child 0
10
+ m/0' -> hardened child 0
11
+ m/44'/0'/0'/0 -> BIP-44 first receive address
12
+
13
+ Hardened indices are >= 2^31 in raw form. We represent them as
14
+ `(index, hardened)` pairs to keep the API unambiguous.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import hmac
21
+ import struct
22
+ from typing import Final
23
+
24
+ from coincurve import PrivateKey
25
+ from pydantic import BaseModel
26
+
27
+ from keystonecrypto.exceptions import InvalidDerivationPathError
28
+ from keystonecrypto.secret_bytes import SecretBytes
29
+
30
+ __all__ = [
31
+ "DerivationPath",
32
+ "ExtendedPrivateKey",
33
+ "derive_from_seed",
34
+ "HARDENED_OFFSET",
35
+ "SECP256K1_N",
36
+ ]
37
+
38
+ HARDENED_OFFSET: Final[int] = 0x80000000
39
+ _MAX_CHILD_INDEX: Final[int] = 0xFFFFFFFF
40
+
41
+ # Order of the secp256k1 prime-order subgroup.
42
+ SECP256K1_N: Final[int] = (
43
+ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
44
+ )
45
+
46
+
47
+ class DerivationPath(BaseModel):
48
+ """A parsed BIP-32 derivation path."""
49
+
50
+ model_config = {"frozen": True}
51
+
52
+ indices: tuple[tuple[int, bool], ...] # (index, hardened) per component
53
+
54
+ @classmethod
55
+ def parse(cls, s: str) -> "DerivationPath":
56
+ s = s.strip()
57
+ if s in ("", "m"):
58
+ return cls(indices=())
59
+ if not s.startswith("m/"):
60
+ raise InvalidDerivationPathError(
61
+ f"path must start with 'm/', got {s!r}"
62
+ )
63
+ parts = s[2:].split("/")
64
+ out: list[tuple[int, bool]] = []
65
+ for part in parts:
66
+ hardened = part.endswith("'") or part.endswith("h")
67
+ num_str = part.rstrip("'h")
68
+ if not num_str.isdigit():
69
+ raise InvalidDerivationPathError(f"non-numeric component: {part!r}")
70
+ idx = int(num_str)
71
+ if idx < 0 or idx > 0x7FFFFFFF:
72
+ raise InvalidDerivationPathError(f"index out of range: {idx}")
73
+ out.append((idx, hardened))
74
+ return cls(indices=tuple(out))
75
+
76
+
77
+ class ExtendedPrivateKey:
78
+ """A BIP-32 extended private key (xprv): key + chain code + metadata."""
79
+
80
+ __slots__ = ("_sk", "_cc", "_depth", "_pfp", "_child")
81
+
82
+ def __init__(
83
+ self,
84
+ secret_key: SecretBytes,
85
+ chain_code: SecretBytes,
86
+ depth: int,
87
+ parent_fingerprint: bytes,
88
+ child_number: int,
89
+ ) -> None:
90
+ self._sk = secret_key
91
+ self._cc = chain_code
92
+ self._depth = depth
93
+ self._pfp = parent_fingerprint
94
+ self._child = child_number
95
+
96
+ @classmethod
97
+ def master(cls, seed: SecretBytes) -> "ExtendedPrivateKey":
98
+ I = hmac.new(b"Bitcoin seed", bytes(seed), hashlib.sha512).digest()
99
+ IL, IR = I[:32], I[32:]
100
+ return cls(
101
+ secret_key=SecretBytes(IL),
102
+ chain_code=SecretBytes(IR),
103
+ depth=0,
104
+ parent_fingerprint=b"\x00\x00\x00\x00",
105
+ child_number=0,
106
+ )
107
+
108
+ @property
109
+ def private_key(self) -> SecretBytes:
110
+ return self._sk
111
+
112
+ @property
113
+ def chain_code(self) -> SecretBytes:
114
+ return self._cc
115
+
116
+ @property
117
+ def depth(self) -> int:
118
+ return self._depth
119
+
120
+ @property
121
+ def child_number(self) -> int:
122
+ return self._child
123
+
124
+ def fingerprint(self) -> bytes:
125
+ """BIP-32 key fingerprint = first 4 bytes of HASH160(pubkey)."""
126
+ pk = PrivateKey(bytes(self._sk)).public_key.format(compressed=True)
127
+ h160 = hashlib.new("ripemd160", hashlib.sha256(pk).digest()).digest()
128
+ return h160[:4]
129
+
130
+ @property
131
+ def parent_fingerprint(self) -> bytes:
132
+ return self._pfp
133
+
134
+ def derive(self, index: int, hardened: bool = False) -> "ExtendedPrivateKey":
135
+ if index < 0 or index > 0x7FFFFFFF:
136
+ raise ValueError(f"index out of range: {index}")
137
+ if hardened:
138
+ index += HARDENED_OFFSET
139
+ if index < 0 or index > _MAX_CHILD_INDEX:
140
+ raise ValueError(f"effective child index out of range: {index}")
141
+
142
+ if index >= HARDENED_OFFSET:
143
+ # Hardened: data = 0x00 || ser256(kpar) || ser32(i)
144
+ data = b"\x00" + bytes(self._sk) + struct.pack(">I", index)
145
+ else:
146
+ # Non-hardened: data = serP(Kpar) || ser32(i)
147
+ pub = PrivateKey(bytes(self._sk)).public_key.format(compressed=True)
148
+ data = pub + struct.pack(">I", index)
149
+
150
+ I = hmac.new(bytes(self._cc), data, hashlib.sha512).digest()
151
+ IL, IR = I[:32], I[32:]
152
+ # child_key = (IL + kpar) mod n
153
+ Il_int = int.from_bytes(IL, "big")
154
+ kpar_int = int.from_bytes(bytes(self._sk), "big")
155
+ if Il_int >= SECP256K1_N:
156
+ raise ValueError("derivation produced invalid child key (IL >= n)")
157
+ child_int = (Il_int + kpar_int) % SECP256K1_N
158
+ if child_int == 0:
159
+ raise ValueError("derivation produced zero child key")
160
+ child_key = child_int.to_bytes(32, "big")
161
+
162
+ return ExtendedPrivateKey(
163
+ secret_key=SecretBytes(child_key),
164
+ chain_code=SecretBytes(IR),
165
+ depth=self._depth + 1,
166
+ parent_fingerprint=self.fingerprint(),
167
+ child_number=index,
168
+ )
169
+
170
+ def to_base58(self, network: str = "mainnet") -> str:
171
+ """Serialize as a BIP-32 xprv (private key form)."""
172
+ version = {
173
+ "mainnet": b"\x04\x88\xad\xe4",
174
+ "testnet": b"\x04\x35\x83\x94",
175
+ }[network]
176
+ raw = (
177
+ version
178
+ + bytes([self._depth])
179
+ + self._pfp
180
+ + struct.pack(">I", self._child)
181
+ + bytes(self._cc)
182
+ + b"\x00"
183
+ + bytes(self._sk)
184
+ )
185
+ # Double-SHA256 checksum.
186
+ checksum = hashlib.sha256(hashlib.sha256(raw).digest()).digest()[:4]
187
+ return _b58encode(raw + checksum)
188
+
189
+
190
+ def derive_from_seed(seed: SecretBytes, path: DerivationPath) -> ExtendedPrivateKey:
191
+ """Derive an extended private key from a seed and a BIP-32 path."""
192
+ key: ExtendedPrivateKey = ExtendedPrivateKey.master(seed)
193
+ for index, hardened in path.indices:
194
+ key = key.derive(index, hardened=hardened)
195
+ return key
196
+
197
+
198
+ # --- base58 (minimal; avoids an extra dep for the core) ---
199
+
200
+ _B58_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
201
+
202
+
203
+ def _b58encode(data: bytes) -> str:
204
+ n = int.from_bytes(data, "big")
205
+ if n == 0:
206
+ return _B58_ALPHABET[0:1].decode()
207
+ out = bytearray()
208
+ while n:
209
+ n, r = divmod(n, 58)
210
+ out.append(_B58_ALPHABET[r])
211
+ # Preserve leading zero bytes.
212
+ for b in data:
213
+ if b == 0:
214
+ out.append(_B58_ALPHABET[0])
215
+ else:
216
+ break
217
+ out.reverse()
218
+ return out.decode()
@@ -0,0 +1,38 @@
1
+ """Cryptographically-secure entropy generation.
2
+
3
+ Wraps `secrets.token_bytes` and returns a SecretBytes. Validates bit
4
+ lengths to match the BIP-39 allowed strengths so the same module can
5
+ feed both raw entropy generation and mnemonic generation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import secrets
11
+
12
+ from keystonecrypto.secret_bytes import SecretBytes
13
+
14
+ __all__ = ["STRENGTH_BITS", "generate_entropy"]
15
+
16
+ # BIP-39 § "Entropy" — allowed entropy sizes in bits.
17
+ STRENGTH_BITS: tuple[int, ...] = (128, 160, 192, 224, 256)
18
+
19
+ _MIN_BITS = min(STRENGTH_BITS)
20
+ _MAX_BITS = max(STRENGTH_BITS)
21
+
22
+
23
+ def generate_entropy(bits: int) -> SecretBytes:
24
+ """Generate `bits` bits of cryptographically-secure random data.
25
+
26
+ Returns: SecretBytes of length `bits // 8`.
27
+
28
+ Raises:
29
+ ValueError: if `bits` is not a multiple of 8 or is outside the
30
+ BIP-39 allowed range.
31
+ """
32
+ if bits % 8 != 0:
33
+ raise ValueError(f"bits must be a multiple of 8, got {bits}")
34
+ if bits < _MIN_BITS or bits > _MAX_BITS:
35
+ raise ValueError(
36
+ f"bits must be in [{_MIN_BITS}, {_MAX_BITS}], got {bits}"
37
+ )
38
+ return SecretBytes(secrets.token_bytes(bits // 8))
@@ -0,0 +1,210 @@
1
+ """Binary envelope for encrypted keystores (spec §6).
2
+
3
+ Layout (big-endian, exact byte counts):
4
+
5
+ Header (66 bytes):
6
+ magic: 4 "KST1"
7
+ version: 1 0x01
8
+ kdf_id: 1 0x01 = Argon2id
9
+ kdf_params: 32 (time_cost:4 | memory_cost:4 |
10
+ parallelism:4 | hash_len:4 |
11
+ version:4 | reserved:12)
12
+ salt: 16
13
+ nonce: 12
14
+ Body (variable):
15
+ ciphertext: N (includes appended 16-byte GCM tag)
16
+
17
+ Plaintext structure before encryption:
18
+
19
+ seed_length: 1
20
+ seed: seed_length
21
+ created_at: 8 (BE unix timestamp)
22
+ flags: 1 (bit 0: has_passphrase)
23
+ reserved: 6 (zero)
24
+
25
+ AAD = version || kdf_id. Decryption with mismatched AAD fails.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import secrets as _secrets
31
+ import struct
32
+ import time
33
+ from typing import Final
34
+
35
+ from pydantic import BaseModel
36
+
37
+ from keystonecrypto.aead import NONCE_LEN, decrypt as _decrypt
38
+ from keystonecrypto.aead import encrypt as _encrypt
39
+ from keystonecrypto.exceptions import (
40
+ KeystoreDecryptionError,
41
+ KeystoreVersionError,
42
+ )
43
+ from keystonecrypto.kdf import Argon2Params, derive_key
44
+ from keystonecrypto.secret_bytes import SecretBytes
45
+
46
+ __all__ = [
47
+ "MAGIC",
48
+ "VERSION",
49
+ "Envelope",
50
+ "UnpackedKeystore",
51
+ "HEADER_LEN",
52
+ ]
53
+
54
+ MAGIC: Final[bytes] = b"KST1"
55
+ VERSION: Final[int] = 1
56
+
57
+ _HEADER_LEN: Final[int] = 4 + 1 + 1 + 32 + 16 + NONCE_LEN # = 66
58
+ _GCM_TAG_LEN: Final[int] = 16
59
+ _PLAINTEXT_OVERHEAD: Final[int] = 1 + 8 + 1 + 6 # = 16 (excluding seed)
60
+
61
+ HEADER_LEN: Final[int] = _HEADER_LEN
62
+
63
+ # Offsets inside the header.
64
+ _OFF_MAGIC: Final[int] = 0
65
+ _OFF_VERSION: Final[int] = 4
66
+ _OFF_KDF_ID: Final[int] = 5
67
+ _OFF_KDF: Final[int] = 6
68
+ _OFF_SALT: Final[int] = 38
69
+ _OFF_NONCE: Final[int] = 54
70
+
71
+
72
+ class UnpackedKeystore(BaseModel):
73
+ """Result of a successful envelope decryption."""
74
+
75
+ model_config = {"frozen": True}
76
+
77
+ seed: SecretBytes
78
+ has_passphrase: bool
79
+ created_at: int
80
+
81
+
82
+ class Envelope:
83
+ """Static pack/unpack helpers for the keystore binary envelope."""
84
+
85
+ @staticmethod
86
+ def _pack_kdf_params(p: Argon2Params) -> bytes:
87
+ return struct.pack(
88
+ ">IIII I 12x",
89
+ p.time_cost, p.memory_cost, p.parallelism, p.hash_len,
90
+ p.version,
91
+ )
92
+
93
+ @staticmethod
94
+ def _unpack_kdf_params(blob: bytes) -> Argon2Params:
95
+ # Matches `_pack_kdf_params`: 5 unsigned ints (20 bytes) + 12 bytes reserved.
96
+ # Total 32 bytes.
97
+ if len(blob) != 32:
98
+ raise KeystoreDecryptionError(f"kdf_params section wrong size: {len(blob)} bytes")
99
+ (t, m, par, hl, v) = struct.unpack(">IIIII 12x", blob)
100
+ return Argon2Params.for_testing(
101
+ time_cost=t, memory_cost=m, parallelism=par,
102
+ hash_len=hl, salt_len=16, version=v,
103
+ )
104
+
105
+ @staticmethod
106
+ def _aad(version: int, kdf_id: int) -> bytes:
107
+ return bytes([version, kdf_id])
108
+
109
+ @staticmethod
110
+ def pack(
111
+ seed: SecretBytes,
112
+ password: SecretBytes,
113
+ params: Argon2Params,
114
+ kdf_id: int = 1,
115
+ has_passphrase: bool = False,
116
+ created_at: int | None = None,
117
+ ) -> bytes:
118
+ """Pack and encrypt a seed into a keystore blob.
119
+
120
+ The caller passes `password` here. If a BIP-39 passphrase is
121
+ used, the caller is expected to concatenate it with the
122
+ decryption password *before* calling pack; the envelope itself
123
+ is passphrase-agnostic.
124
+ """
125
+ if kdf_id != 1:
126
+ raise ValueError(f"unsupported kdf_id {kdf_id} (only 1 = Argon2id)")
127
+ if len(seed) > 255:
128
+ raise ValueError(f"seed too long: {len(seed)} > 255 bytes")
129
+ if params.hash_len != 32:
130
+ raise ValueError(f"envelope requires hash_len=32, got {params.hash_len}")
131
+
132
+ salt = _secrets.token_bytes(params.salt_len)
133
+ key, _salt = derive_key(password, params, salt=salt)
134
+ ts = int(created_at if created_at is not None else time.time())
135
+ flags = 0x01 if has_passphrase else 0x00
136
+ plaintext = (
137
+ bytes([len(seed)])
138
+ + bytes(seed)
139
+ + struct.pack(">q", ts)
140
+ + bytes([flags])
141
+ + b"\x00" * 6
142
+ )
143
+ nonce, ciphertext = _encrypt(key, plaintext, aad=Envelope._aad(VERSION, kdf_id))
144
+
145
+ return (
146
+ MAGIC
147
+ + bytes([VERSION, kdf_id])
148
+ + Envelope._pack_kdf_params(params)
149
+ + salt
150
+ + nonce
151
+ + ciphertext
152
+ )
153
+
154
+ @staticmethod
155
+ def unpack(blob: bytes, password: SecretBytes) -> UnpackedKeystore:
156
+ """Decrypt and validate a keystore blob."""
157
+ if len(blob) < _HEADER_LEN + _GCM_TAG_LEN:
158
+ raise KeystoreDecryptionError(f"blob too short: {len(blob)} bytes")
159
+
160
+ if blob[_OFF_MAGIC:_OFF_MAGIC + 4] != MAGIC:
161
+ raise KeystoreVersionError("bad magic; not a keystonecrypto keystore")
162
+
163
+ version = blob[_OFF_VERSION]
164
+ if version != VERSION:
165
+ raise KeystoreVersionError(
166
+ f"unsupported keystore version {version}; "
167
+ f"this build of keystonecrypto only supports version {VERSION}"
168
+ )
169
+
170
+ kdf_id = blob[_OFF_KDF_ID]
171
+
172
+ params = Envelope._unpack_kdf_params(blob[_OFF_KDF:_OFF_KDF + 32])
173
+ if params.hash_len != _GCM_TAG_LEN + _GCM_TAG_LEN:
174
+ pass # hash_len != 32 will be caught at KDF time
175
+ salt = blob[_OFF_SALT:_OFF_SALT + 16]
176
+ nonce = blob[_OFF_NONCE:_OFF_NONCE + NONCE_LEN]
177
+ ciphertext = blob[_HEADER_LEN:]
178
+
179
+ # kdf_id bound into the AAD so tampering causes AEAD failure
180
+ # (KeystoreDecryptionError), not an early format rejection.
181
+ # We still surface an unsupported-kdf_id error if AEAD happens
182
+ # to succeed somehow (e.g. crafted blob).
183
+ key, _ = derive_key(password, params, salt=salt)
184
+
185
+ try:
186
+ plaintext = _decrypt(key, nonce, ciphertext,
187
+ aad=Envelope._aad(version, kdf_id))
188
+ except KeystoreDecryptionError:
189
+ raise
190
+
191
+ if kdf_id != 1:
192
+ raise KeystoreVersionError(f"unsupported kdf_id {kdf_id}")
193
+
194
+ return Envelope._unpack_plaintext(plaintext)
195
+
196
+ @staticmethod
197
+ def _unpack_plaintext(plaintext: bytes) -> UnpackedKeystore:
198
+ if len(plaintext) < 1 + 8 + 1 + 6:
199
+ raise KeystoreDecryptionError("plaintext too short")
200
+ seed_len = plaintext[0]
201
+ if len(plaintext) < 1 + seed_len + 8 + 1 + 6:
202
+ raise KeystoreDecryptionError("seed length inconsistent")
203
+ seed = SecretBytes(plaintext[1:1 + seed_len])
204
+ created_at = struct.unpack(">q", plaintext[1 + seed_len:1 + seed_len + 8])[0]
205
+ flags = plaintext[1 + seed_len + 8]
206
+ return UnpackedKeystore(
207
+ seed=seed,
208
+ has_passphrase=bool(flags & 0x01),
209
+ created_at=created_at,
210
+ )
@@ -0,0 +1,41 @@
1
+ """Exception hierarchy for keystonecrypto."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "KeystoneCryptoError",
7
+ "InvalidMnemonicError",
8
+ "InvalidPassphraseError",
9
+ "InvalidDerivationPathError",
10
+ "KeystoreVersionError",
11
+ "KeystoreDecryptionError",
12
+ "SignatureError",
13
+ ]
14
+
15
+
16
+ class KeystoneCryptoError(Exception):
17
+ """Base exception for all keystonecrypto errors."""
18
+
19
+
20
+ class InvalidMnemonicError(KeystoneCryptoError):
21
+ """Mnemonic word, length, or checksum is invalid."""
22
+
23
+
24
+ class InvalidPassphraseError(KeystoneCryptoError):
25
+ """BIP-39 passphrase is malformed (e.g. invalid UTF-8 after NFKD)."""
26
+
27
+
28
+ class InvalidDerivationPathError(KeystoneCryptoError):
29
+ """BIP-32 derivation path cannot be parsed or is out of range."""
30
+
31
+
32
+ class KeystoreVersionError(KeystoneCryptoError):
33
+ """Keystore envelope version is not supported by this library version."""
34
+
35
+
36
+ class KeystoreDecryptionError(KeystoneCryptoError):
37
+ """Keystore decryption failed: wrong passphrase, tampered ciphertext, or corrupt header."""
38
+
39
+
40
+ class SignatureError(KeystoneCryptoError):
41
+ """Signing operation failed or signature could not be verified."""