iwa 0.0.0__py3-none-any.whl → 0.0.1a2__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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
iwa/core/mnemonic.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""BIP-39 mnemonic generator, encrypt/decrypt, ETH account derivation and keystore saving."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
from bip_utils import (
|
|
10
|
+
Bip39MnemonicGenerator,
|
|
11
|
+
Bip39SeedGenerator,
|
|
12
|
+
Bip39WordsNum,
|
|
13
|
+
Bip44,
|
|
14
|
+
Bip44Changes,
|
|
15
|
+
Bip44Coins,
|
|
16
|
+
)
|
|
17
|
+
from cryptography.exceptions import InvalidTag
|
|
18
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
19
|
+
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|
20
|
+
from eth_account import Account
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
|
|
23
|
+
from iwa.core.constants import WALLET_PATH
|
|
24
|
+
from iwa.core.models import EthereumAddress, StoredAccount
|
|
25
|
+
|
|
26
|
+
MNEMONIC_WORD_NUMBER = Bip39WordsNum.WORDS_NUM_24
|
|
27
|
+
SCRYPT_N = 2**14
|
|
28
|
+
SCRYPT_R = 8
|
|
29
|
+
SCRYPT_P = 1
|
|
30
|
+
SCRYPT_LEN = 32
|
|
31
|
+
AES_NONCE_LEN = 12
|
|
32
|
+
SALT_LEN = 16
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EncryptedMnemonic(BaseModel):
|
|
36
|
+
"""EncryptedMnemonic"""
|
|
37
|
+
|
|
38
|
+
kdf: str = "scrypt"
|
|
39
|
+
kdf_salt: str
|
|
40
|
+
kdf_n: int = SCRYPT_N
|
|
41
|
+
kdf_r: int = SCRYPT_R
|
|
42
|
+
kdf_p: int = SCRYPT_P
|
|
43
|
+
kdf_len: int = SCRYPT_LEN
|
|
44
|
+
cypher: str = "aesgcm"
|
|
45
|
+
nonce: str
|
|
46
|
+
ciphertext: str
|
|
47
|
+
|
|
48
|
+
def derive_key(self, password: bytes) -> bytes:
|
|
49
|
+
"""Derive a key from a password and salt using scrypt."""
|
|
50
|
+
kdf = Scrypt(
|
|
51
|
+
salt=base64.b64decode(self.kdf_salt),
|
|
52
|
+
length=self.kdf_len,
|
|
53
|
+
n=self.kdf_n,
|
|
54
|
+
r=self.kdf_r,
|
|
55
|
+
p=self.kdf_p,
|
|
56
|
+
)
|
|
57
|
+
return kdf.derive(password)
|
|
58
|
+
|
|
59
|
+
def decrypt(self, password: str) -> str:
|
|
60
|
+
"""Decrypt an object."""
|
|
61
|
+
# validate expected algorithms
|
|
62
|
+
if self.kdf != "scrypt":
|
|
63
|
+
raise ValueError(f"Unsupported kdf: {self.kdf}")
|
|
64
|
+
if self.cypher != "aesgcm":
|
|
65
|
+
raise ValueError("Unsupported cipher, expected 'aesgcm'")
|
|
66
|
+
|
|
67
|
+
nonce = base64.b64decode(self.nonce)
|
|
68
|
+
ct = base64.b64decode(self.ciphertext)
|
|
69
|
+
|
|
70
|
+
# derive key using the parameters from the file
|
|
71
|
+
key = self.derive_key(password.encode("utf-8"))
|
|
72
|
+
aesgcm = AESGCM(key)
|
|
73
|
+
pt = aesgcm.decrypt(nonce, ct, None)
|
|
74
|
+
return pt.decode("utf-8")
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def encrypt(cls, mnemonic: str, password: str) -> dict:
|
|
78
|
+
"""Encrypt a mnemonic with AES-GCM using a scrypt-derived key."""
|
|
79
|
+
password_b = password.encode("utf-8")
|
|
80
|
+
salt = os.urandom(SALT_LEN)
|
|
81
|
+
# Create a temporary instance to use derive_key or use the class method if refactored
|
|
82
|
+
# But derive_key is an instance method using instance attributes.
|
|
83
|
+
# We should probably just use Scrypt directly here or refactor derive_key.
|
|
84
|
+
# Since derive_key uses self.kdf_n etc, we can use the constants.
|
|
85
|
+
|
|
86
|
+
kdf = Scrypt(
|
|
87
|
+
salt=salt,
|
|
88
|
+
length=SCRYPT_LEN,
|
|
89
|
+
n=SCRYPT_N,
|
|
90
|
+
r=SCRYPT_R,
|
|
91
|
+
p=SCRYPT_P,
|
|
92
|
+
)
|
|
93
|
+
key = kdf.derive(password_b)
|
|
94
|
+
|
|
95
|
+
aesgcm = AESGCM(key)
|
|
96
|
+
nonce = os.urandom(AES_NONCE_LEN)
|
|
97
|
+
ct = aesgcm.encrypt(nonce, mnemonic.encode("utf-8"), None)
|
|
98
|
+
return {
|
|
99
|
+
"kdf": "scrypt",
|
|
100
|
+
"kdf_salt": base64.b64encode(salt).decode(),
|
|
101
|
+
"kdf_n": SCRYPT_N,
|
|
102
|
+
"kdf_r": SCRYPT_R,
|
|
103
|
+
"kdf_p": SCRYPT_P,
|
|
104
|
+
"kdf_len": SCRYPT_LEN,
|
|
105
|
+
"cipher": "aesgcm",
|
|
106
|
+
"nonce": base64.b64encode(nonce).decode(),
|
|
107
|
+
"ciphertext": base64.b64encode(ct).decode(),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class MnemonicStorage(BaseModel):
|
|
112
|
+
"""MnemonicStorage"""
|
|
113
|
+
|
|
114
|
+
encrypted_mnemonic: EncryptedMnemonic
|
|
115
|
+
accounts: Dict[EthereumAddress, StoredAccount] = {}
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def load(file_path: Path = WALLET_PATH) -> "MnemonicStorage":
|
|
119
|
+
"""Load"""
|
|
120
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
121
|
+
data = json.load(f)
|
|
122
|
+
data["encrypted_mnemonic"] = EncryptedMnemonic(**data["encrypted_mnemonic"])
|
|
123
|
+
data["accounts"] = {k: StoredAccount(**v) for k, v in data.get("accounts", {}).items()}
|
|
124
|
+
return MnemonicStorage(**data)
|
|
125
|
+
|
|
126
|
+
def save(self, file_path: Path = WALLET_PATH) -> None:
|
|
127
|
+
"""Save mnemonic storage to file with secure permissions."""
|
|
128
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
129
|
+
json.dump(self.model_dump(), f, indent=4)
|
|
130
|
+
# SECURITY: Restrict file permissions to owner only
|
|
131
|
+
os.chmod(file_path, 0o600)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MnemonicManager:
|
|
135
|
+
"""Manager for BIP-39 mnemonics and keystore operations.
|
|
136
|
+
|
|
137
|
+
Provides methods to generate mnemonics, encrypt/decrypt them using
|
|
138
|
+
scrypt + AES-GCM, derive Ethereum accounts (BIP-44), and save
|
|
139
|
+
keystores to disk.
|
|
140
|
+
|
|
141
|
+
Attributes:
|
|
142
|
+
mnemonic_file (str): Default file path for the encrypted mnemonic.
|
|
143
|
+
mnemonic_word_number (Bip39WordsNum): Number of words in the mnemonic.
|
|
144
|
+
scrypt_*: Parameters for the scrypt KDF.
|
|
145
|
+
aes_nonce_len (int): Nonce length for AES-GCM.
|
|
146
|
+
salt_len (int): Salt length for scrypt.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
mnemonic_file: str = WALLET_PATH,
|
|
153
|
+
mnemonic_word_number: Bip39WordsNum = MNEMONIC_WORD_NUMBER,
|
|
154
|
+
scrypt_n: int = SCRYPT_N,
|
|
155
|
+
scrypt_r: int = SCRYPT_R,
|
|
156
|
+
scrypt_p: int = SCRYPT_P,
|
|
157
|
+
scrypt_len: int = SCRYPT_LEN,
|
|
158
|
+
aes_nonce_len: int = AES_NONCE_LEN,
|
|
159
|
+
salt_len: int = SALT_LEN,
|
|
160
|
+
):
|
|
161
|
+
"""Initialize MnemonicManager with configuration parameters."""
|
|
162
|
+
self.mnemonic_file = mnemonic_file
|
|
163
|
+
self.mnemonic_word_number = mnemonic_word_number
|
|
164
|
+
self.scrypt_n = scrypt_n
|
|
165
|
+
self.scrypt_r = scrypt_r
|
|
166
|
+
self.scrypt_p = scrypt_p
|
|
167
|
+
self.scrypt_len = scrypt_len
|
|
168
|
+
self.aes_nonce_len = aes_nonce_len
|
|
169
|
+
self.salt_len = salt_len
|
|
170
|
+
|
|
171
|
+
def derive_key(
|
|
172
|
+
self,
|
|
173
|
+
password: bytes,
|
|
174
|
+
salt: bytes,
|
|
175
|
+
n: int | None = None,
|
|
176
|
+
r: int | None = None,
|
|
177
|
+
p: int | None = None,
|
|
178
|
+
length: int | None = None,
|
|
179
|
+
) -> bytes:
|
|
180
|
+
"""Derive a key from a password and salt using scrypt.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
password (bytes): The password in bytes.
|
|
184
|
+
salt (bytes): A random salt.
|
|
185
|
+
n (int | None): CPU/memory cost factor.
|
|
186
|
+
r (int | None): Block size parameter.
|
|
187
|
+
p (int | None): Parallelization parameter.
|
|
188
|
+
length (int | None): Desired key length in bytes.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
bytes: The derived key of length `self.scrypt_len`.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
# use provided parameters or fall back to instance defaults
|
|
195
|
+
n = n if n is not None else self.scrypt_n
|
|
196
|
+
r = r if r is not None else self.scrypt_r
|
|
197
|
+
p = p if p is not None else self.scrypt_p
|
|
198
|
+
length = length if length is not None else self.scrypt_len
|
|
199
|
+
|
|
200
|
+
kdf = Scrypt(
|
|
201
|
+
salt=salt,
|
|
202
|
+
length=length,
|
|
203
|
+
n=n,
|
|
204
|
+
r=r,
|
|
205
|
+
p=p,
|
|
206
|
+
)
|
|
207
|
+
return kdf.derive(password)
|
|
208
|
+
|
|
209
|
+
def encrypt_mnemonic(self, mnemonic: str, password: str) -> dict:
|
|
210
|
+
"""Encrypt a mnemonic with AES-GCM using a scrypt-derived key.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
mnemonic (str): The mnemonic as plain text.
|
|
214
|
+
password (str): Password used to derive the encryption key.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
dict: JSON-serializable object containing KDF params, nonce,
|
|
218
|
+
and ciphertext (all base64-encoded).
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
password_b = password.encode("utf-8")
|
|
222
|
+
salt = os.urandom(self.salt_len)
|
|
223
|
+
key = self.derive_key(password_b, salt)
|
|
224
|
+
aesgcm = AESGCM(key)
|
|
225
|
+
nonce = os.urandom(self.aes_nonce_len)
|
|
226
|
+
ct = aesgcm.encrypt(nonce, mnemonic.encode("utf-8"), None)
|
|
227
|
+
return {
|
|
228
|
+
"kdf": "scrypt",
|
|
229
|
+
"kdf_salt": base64.b64encode(salt).decode(),
|
|
230
|
+
"kdf_n": self.scrypt_n,
|
|
231
|
+
"kdf_r": self.scrypt_r,
|
|
232
|
+
"kdf_p": self.scrypt_p,
|
|
233
|
+
"kdf_len": self.scrypt_len,
|
|
234
|
+
"cipher": "aesgcm",
|
|
235
|
+
"nonce": base64.b64encode(nonce).decode(),
|
|
236
|
+
"ciphertext": base64.b64encode(ct).decode(),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def decrypt_mnemonic(self, encobj: dict, password: str) -> str:
|
|
240
|
+
"""Decrypt an object previously created by `encrypt_mnemonic`.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
encobj (dict): Object with KDF params, nonce and ciphertext
|
|
244
|
+
encoded in base64.
|
|
245
|
+
password (str): Password to derive the decryption key.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
str: The mnemonic in plain text.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
# validate expected algorithms
|
|
252
|
+
kdf_name = encobj.get("kdf", "scrypt")
|
|
253
|
+
if kdf_name != "scrypt":
|
|
254
|
+
raise ValueError(f"Unsupported kdf: {kdf_name}")
|
|
255
|
+
if encobj.get("cipher", "aesgcm") != "aesgcm":
|
|
256
|
+
raise ValueError("Unsupported cipher, expected 'aesgcm'")
|
|
257
|
+
|
|
258
|
+
salt = base64.b64decode(encobj["kdf_salt"])
|
|
259
|
+
nonce = base64.b64decode(encobj["nonce"])
|
|
260
|
+
ct = base64.b64decode(encobj["ciphertext"])
|
|
261
|
+
|
|
262
|
+
# read kdf params from the encoded object, falling back to defaults
|
|
263
|
+
n = int(encobj.get("kdf_n", self.scrypt_n))
|
|
264
|
+
r = int(encobj.get("kdf_r", self.scrypt_r))
|
|
265
|
+
p = int(encobj.get("kdf_p", self.scrypt_p))
|
|
266
|
+
length = int(encobj.get("kdf_len", self.scrypt_len))
|
|
267
|
+
|
|
268
|
+
# derive key using the parameters from the file
|
|
269
|
+
key = self.derive_key(password.encode("utf-8"), salt, n=n, r=r, p=p, length=length)
|
|
270
|
+
aesgcm = AESGCM(key)
|
|
271
|
+
pt = aesgcm.decrypt(nonce, ct, None)
|
|
272
|
+
return pt.decode("utf-8")
|
|
273
|
+
|
|
274
|
+
def generate_and_store_mnemonic(
|
|
275
|
+
self,
|
|
276
|
+
password: str,
|
|
277
|
+
out_file: str = None,
|
|
278
|
+
) -> str:
|
|
279
|
+
"""Generate a BIP-39 mnemonic, encrypt it and save to disk.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
password (str): Password to encrypt the mnemonic.
|
|
283
|
+
out_file (str): Destination file. Optional; if None this method
|
|
284
|
+
uses `self.mnemonic_file`.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
str: The plaintext mnemonic (returned so data is available).
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
out_file = out_file or self.mnemonic_file
|
|
291
|
+
mnemonic = Bip39MnemonicGenerator().FromWordsNumber(self.mnemonic_word_number)
|
|
292
|
+
mnemonic_str = mnemonic.ToStr()
|
|
293
|
+
enc = self.encrypt_mnemonic(mnemonic_str, password)
|
|
294
|
+
with open(out_file, "w", encoding="utf-8") as f:
|
|
295
|
+
json.dump(enc, f, indent=2)
|
|
296
|
+
os.chmod(out_file, 0o600)
|
|
297
|
+
return mnemonic_str
|
|
298
|
+
|
|
299
|
+
def load_and_decrypt_mnemonic(
|
|
300
|
+
self,
|
|
301
|
+
password: str,
|
|
302
|
+
in_file: str = None,
|
|
303
|
+
) -> str:
|
|
304
|
+
"""Load and decrypt a mnemonic from a file.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
password (str): Password to decrypt the mnemonic.
|
|
308
|
+
in_file (str): File path with the encrypted object. Optional;
|
|
309
|
+
if None `self.mnemonic_file` is used.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
str: The plaintext mnemonic.
|
|
313
|
+
|
|
314
|
+
"""
|
|
315
|
+
in_file = in_file or self.mnemonic_file
|
|
316
|
+
with open(in_file, "r", encoding="utf-8") as f:
|
|
317
|
+
enc = json.load(f)
|
|
318
|
+
try:
|
|
319
|
+
mnemonic = self.decrypt_mnemonic(enc, password)
|
|
320
|
+
return mnemonic
|
|
321
|
+
except InvalidTag as e:
|
|
322
|
+
raise ValueError("Incorrect password") from e
|
|
323
|
+
|
|
324
|
+
def derive_eth_accounts_from_mnemonic(
|
|
325
|
+
self,
|
|
326
|
+
password: str,
|
|
327
|
+
n_accounts: int = 5,
|
|
328
|
+
):
|
|
329
|
+
"""Derive Ethereum accounts (BIP-44) from a BIP-39 mnemonic.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
password (str): Password to decrypt the mnemonic.
|
|
333
|
+
n_accounts (int): Number of accounts to derive.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
list: Dicts with keys 'index', 'address' and 'private_key_hex'.
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
mnemonic = self.load_and_decrypt_mnemonic(password)
|
|
340
|
+
|
|
341
|
+
if mnemonic is None:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
accounts = []
|
|
345
|
+
for i in range(n_accounts):
|
|
346
|
+
# obtain private key (hex) for this index using helper
|
|
347
|
+
priv_hex = self.derive_private_key_hex_from_mnemonic(mnemonic, i)
|
|
348
|
+
priv_bytes = bytes.fromhex(priv_hex)
|
|
349
|
+
acct = Account.from_key(priv_bytes)
|
|
350
|
+
accounts.append(
|
|
351
|
+
{
|
|
352
|
+
"index": i,
|
|
353
|
+
"address": acct.address,
|
|
354
|
+
"private_key_hex": priv_hex,
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
del priv_bytes, acct, priv_hex # clean up sensitive data
|
|
358
|
+
return accounts
|
|
359
|
+
|
|
360
|
+
def derive_private_key_hex_from_mnemonic(
|
|
361
|
+
self,
|
|
362
|
+
mnemonic: str,
|
|
363
|
+
index: int,
|
|
364
|
+
account: int = 0,
|
|
365
|
+
change: Bip44Changes = Bip44Changes.CHAIN_EXT,
|
|
366
|
+
) -> str:
|
|
367
|
+
"""Derive the private key (hex) for a given account index from a mnemonic.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
mnemonic (str): The plaintext BIP-39 mnemonic.
|
|
371
|
+
index (int): Address index to derive.
|
|
372
|
+
account (int): BIP-44 account index (default 0).
|
|
373
|
+
change (Bip44Changes): Change chain (external/internal).
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
str: Private key as a hex string (no 0x prefix).
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
seed_bytes = Bip39SeedGenerator(mnemonic).Generate()
|
|
380
|
+
bip44_mst = Bip44.FromSeed(seed_bytes, Bip44Coins.ETHEREUM)
|
|
381
|
+
# Build the context step by step to avoid long lines
|
|
382
|
+
ctx = bip44_mst.Purpose().Coin().Account(account)
|
|
383
|
+
ctx = ctx.Change(change)
|
|
384
|
+
addr_ctx = ctx.AddressIndex(index)
|
|
385
|
+
return addr_ctx.PrivateKey().Raw().ToHex()
|