22import codecs
33import hashlib
44import itertools
5+ import json
56import logging
67import math
78import re
1516
1617import hdwallet
1718import scrypt
19+ try :
20+ import eth_account
21+ except ImportError :
22+ eth_account = None
1823
1924from .defaults import BITS_DEFAULT , BITS , MNEM_ROWS_COLS , GROUP_REQUIRED_RATIO
2025from .util import ordinal
@@ -64,15 +69,16 @@ class Account( hdwallet.HDWallet ):
6469 | DOGE | Legacy | m/44'/ 3'/0'/0/0 | D... |
6570
6671 """
67- CRYPTOCURRENCIES = ('ETH' , 'BTC' , 'LTC' , 'DOGE' ,) # Currently supported
68- CRYPTO_NAMES = dict (
72+ CRYPTO_NAMES = dict ( # Currently supported
6973 ethereum = 'ETH' ,
7074 bitcoin = 'BTC' ,
7175 litecoin = 'LTC' ,
7276 dogecoin = 'DOGE' ,
7377 )
78+ CRYPTOCURRENCIES = set ( CRYPTO_NAMES .values () )
7479
75- BIP38_CAPABLE = ('BTC' ,)
80+ ETHJS_ENCRYPT = set ( ('ETH' ,) ) # Can be encrypted w/ Ethereum JSON wallet
81+ BIP38_ENCRYPT = CRYPTOCURRENCIES - ETHJS_ENCRYPT # Can be encrypted w/ BIP-38
7682
7783 CRYPTO_FORMAT = dict (
7884 ETH = "legacy" ,
@@ -214,9 +220,29 @@ def from_private_key( self, private_key ):
214220 self .hdwallet .from_private_key ( private_key )
215221 return self
216222
223+ def encrypted ( self , passphrase ):
224+ """Output the appropriately encrypted private key for this cryptocurrency. Ethereum uses
225+ encrypted JSON wallet standard, Bitcoin et.al. use BIP-38 encrypted private keys."""
226+ if self .crypto in self .ETHJS_ENCRYPT :
227+ if not eth_account :
228+ raise NotImplementedError ( "The eth-account package is required to support Ethereum JSON wallet encryption" )
229+ wallet_dict = eth_account .Account .encrypt ( self .key , passphrase )
230+ return json .dumps ( wallet_dict , separators = (',' ,':' ) )
231+ return self .bip38 ( passphrase )
232+
233+ def from_encrypted ( self , encrypted_privkey , passphrase , strict = True ):
234+ """Import the appropriately decrypted private key for this cryptocurrency."""
235+ if self .crypto in self .ETHJS_ENCRYPT :
236+ if not eth_account :
237+ raise NotImplementedError ( "The eth-account package is required to support Ethereum JSON wallet decryption" )
238+ private_hex = bytes ( eth_account .Account .decrypt ( encrypted_privkey , passphrase )).hex ()
239+ self .from_private_key ( private_hex )
240+ return self
241+ return self .from_bip38 ( encrypted_privkey , passphrase = passphrase , strict = strict )
242+
217243 def bip38 ( self , passphrase , flagbyte = b'\xe0 ' ):
218244 """BIP-38 encrypt the private key"""
219- if self .crypto not in self .BIP38_CAPABLE :
245+ if self .crypto not in self .BIP38_ENCRYPT :
220246 raise NotImplementedError ( f"{ self .crypto } does not support BIP-38 private key encryption" )
221247 private_hex = self .key
222248 addr = self .legacy_address ().encode ( 'UTF-8' ) # Eg. b"184xW5g..."
@@ -231,10 +257,14 @@ def bip38( self, passphrase, flagbyte=b'\xe0' ):
231257 enchalf2 = aes .encrypt ( ( int ( private_hex [32 :64 ], 16 ) ^ int .from_bytes ( derivedhalf1 [16 :32 ], 'big' )).to_bytes ( 16 , 'big' ))
232258 prefix = b'\x01 \x42 '
233259 encrypted_privkey = prefix + flagbyte + addrhash + enchalf1 + enchalf2
260+ # Encode the encrypted private key to base58, adding the 4-byte base58 check suffix
234261 return base58 .b58encode_check ( encrypted_privkey ).decode ( 'UTF-8' )
235262
236- def from_bip38 ( self , encrypted_privkey , passphrase ):
237- # Decode the encrypted private key, discarding the 4-byte check suffix
263+ def from_bip38 ( self , encrypted_privkey , passphrase , strict = True ):
264+ """Bip-38 decrypt and import the private key."""
265+ if self .crypto not in self .BIP38_ENCRYPT :
266+ raise NotImplementedError ( f"{ self .crypto } does not support BIP-38 private key decryption" )
267+ # Decode the encrypted private key from base58, discarding the 4-byte base58 check suffix
238268 d = base58 .b58decode_check ( encrypted_privkey )
239269 assert len ( d ) == 43 - 4 , \
240270 f"BIP-38 encrypted key should be 43 bytes long, not { len ( d ) + 4 } bytes"
@@ -258,8 +288,13 @@ def from_bip38( self, encrypted_privkey, passphrase ):
258288 private_hex = codecs .encode ( priv , 'hex_codec' ).decode ( 'ascii' )
259289 self .from_private_key ( private_hex )
260290 addr = self .legacy_address ().encode ( 'UTF-8' ) # Eg. b"184xW5g..."
261- assert hashlib .sha256 ( hashlib .sha256 ( addr ).digest () ).digest ()[0 :4 ] == ahash , \
262- "BIP-38 verification failed; password is incorrect."
291+ ahash_confirm = hashlib .sha256 ( hashlib .sha256 ( addr ).digest () ).digest ()[0 :4 ]
292+ if ahash_confirm != ahash :
293+ warning = f"BIP-38 address hash verification failed ({ ahash_confirm .hex ()} != { ahash .hex ()} ); password may be incorrect."
294+ if strict :
295+ raise AssertionError ( warning )
296+ else :
297+ log .warning ( warning )
263298 return self
264299
265300
0 commit comments