Skip to content

Commit 6ca08e9

Browse files
committed
Make wallet encryption optional again
1 parent aaedd77 commit 6ca08e9

File tree

7 files changed

+39
-19
lines changed

7 files changed

+39
-19
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
run: |
3333
python3 -m pip install -r requirements.txt
3434
python3 -m pip install -r requirements-serial.txt
35-
python3 -m pip install -r requirements-test.txt
35+
python3 -m pip install -r requirements-tests.txt
3636
- name: Lint with flake8
3737
run: |
3838
make analyze || true

requirements-wallet.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
eth-account >=0.6.0, <1
2+
scrypt >=0.8.17

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ fpdf2 >=2.5, <3
44
hdwallet >=2.1, <3
55
mnemonic >=0.19, <1
66
qrcode >=7.3
7-
scrypt >=0.8.17
87
shamir-mnemonic >=0.2, <1

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@
3232
option: open( os.path.join( HERE, f"requirements-{option}.txt" )).readlines()
3333
for option in [
3434
'gui', # slip39[gui]: Support PySimpleGUI/tkinter Graphical UI App
35-
'serial', # slip39[serial]: Support serial I/O of generated wallet data
3635
'dev', # slip39[dev]: All modules to support development
36+
'serial', # slip39[serial]: Support serial I/O of generated wallet data
37+
'wallet', # slip39[wallet]: Paper Wallet and BIP-38/Ethereum wallet encryption
3738
]
3839
}
3940

slip39/api.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
from shamir_mnemonic import generate_mnemonics
1616

1717
import hdwallet
18-
import scrypt
18+
try:
19+
import scrypt
20+
except ImportError:
21+
scrypt = None
1922
try:
2023
import eth_account
2124
except ImportError:
@@ -78,7 +81,7 @@ class Account( hdwallet.HDWallet ):
7881
CRYPTOCURRENCIES = set( CRYPTO_NAMES.values() )
7982

8083
ETHJS_ENCRYPT = set( ('ETH',) ) # Can be encrypted w/ Ethereum JSON wallet
81-
BIP38_ENCRYPT = CRYPTOCURRENCIES - ETHJS_ENCRYPT # Can be encrypted w/ BIP-38
84+
BIP38_ENCRYPT = CRYPTOCURRENCIES - ETHJS_ENCRYPT # Can be encrypted w/ BIP-38
8285

8386
CRYPTO_FORMAT = dict(
8487
ETH = "legacy",
@@ -225,7 +228,7 @@ def encrypted( self, passphrase ):
225228
encrypted JSON wallet standard, Bitcoin et.al. use BIP-38 encrypted private keys."""
226229
if self.crypto in self.ETHJS_ENCRYPT:
227230
if not eth_account:
228-
raise NotImplementedError( "The eth-account package is required to support Ethereum JSON wallet encryption" )
231+
raise NotImplementedError( "The eth-account module is required to support Ethereum JSON wallet encryption; pip install slip39[wallet]" )
229232
wallet_dict = eth_account.Account.encrypt( self.key, passphrase )
230233
return json.dumps( wallet_dict, separators=(',',':') )
231234
return self.bip38( passphrase )
@@ -234,14 +237,16 @@ def from_encrypted( self, encrypted_privkey, passphrase, strict=True ):
234237
"""Import the appropriately decrypted private key for this cryptocurrency."""
235238
if self.crypto in self.ETHJS_ENCRYPT:
236239
if not eth_account:
237-
raise NotImplementedError( "The eth-account package is required to support Ethereum JSON wallet decryption" )
240+
raise NotImplementedError( "The eth-account module is required to support Ethereum JSON wallet decryption; pip install slip39[wallet]" )
238241
private_hex = bytes( eth_account.Account.decrypt( encrypted_privkey, passphrase )).hex()
239242
self.from_private_key( private_hex )
240243
return self
241244
return self.from_bip38( encrypted_privkey, passphrase=passphrase, strict=strict )
242245

243246
def bip38( self, passphrase, flagbyte=b'\xe0' ):
244247
"""BIP-38 encrypt the private key"""
248+
if not scrypt:
249+
raise NotImplementedError( "The scrypt module is required to support BIP-38 encryption; pip install slip39[wallet]" )
245250
if self.crypto not in self.BIP38_ENCRYPT:
246251
raise NotImplementedError( f"{self.crypto} does not support BIP-38 private key encryption" )
247252
private_hex = self.key
@@ -262,6 +267,8 @@ def bip38( self, passphrase, flagbyte=b'\xe0' ):
262267

263268
def from_bip38( self, encrypted_privkey, passphrase, strict=True ):
264269
"""Bip-38 decrypt and import the private key."""
270+
if not scrypt:
271+
raise NotImplementedError( "The scrypt module is required to support BIP-38 decryption; pip install slip39[wallet]" )
265272
if self.crypto not in self.BIP38_ENCRYPT:
266273
raise NotImplementedError( f"{self.crypto} does not support BIP-38 private key decryption" )
267274
# Decode the encrypted private key from base58, discarding the 4-byte base58 check suffix
@@ -290,7 +297,7 @@ def from_bip38( self, encrypted_privkey, passphrase, strict=True ):
290297
addr = self.legacy_address().encode( 'UTF-8' ) # Eg. b"184xW5g..."
291298
ahash_confirm = hashlib.sha256( hashlib.sha256( addr ).digest() ).digest()[0:4]
292299
if ahash_confirm != ahash:
293-
warning = f"BIP-38 address hash verification failed ({ahash_confirm.hex()} != {ahash.hex()}); password may be incorrect."
300+
warning = f"BIP-38 address hash verification failed ({ahash_confirm.hex()} != {ahash.hex()}); password may be incorrect."
294301
if strict:
295302
raise AssertionError( warning )
296303
else:

slip39/api_test.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# -*- mode: python ; coding: utf-8 -*-
22
import json
3+
import pytest
4+
5+
try:
6+
import eth_account
7+
except ImportError:
8+
eth_account = None
9+
try:
10+
import scrypt
11+
except ImportError:
12+
scrypt = None
313

414
import shamir_mnemonic
515

@@ -48,6 +58,8 @@ def test_account():
4858
assert acct.path == "m/44'/3'/0'/0/0"
4959

5060

61+
@pytest.mark.skipif( not scrypt or not eth_account,
62+
reason="pip install slip39[wallet] to support private key encryption" )
5163
def test_account_encrypt():
5264
"""Ensure BIP-38 and Ethereum JSON wallet encryption and recovery works."""
5365

@@ -66,15 +78,14 @@ def test_account_encrypt():
6678
assert acct.path == "m/84'/0'/0'/0/0" # The default; assumed...
6779
assert acct_reco.legacy_address() == "134t1ktyF6e4fNrJR8L6nXtaTENJx9oGcF"
6880

69-
7081
acct = account( SEED_XMAS, crypto='Ethereum' )
7182
assert acct.address == '0x336cBeAB83aCCdb2541e43D514B62DC6C53675f4'
7283
assert acct.crypto == 'ETH'
7384
assert acct.path == "m/44'/60'/0'/0/0"
7485

7586
json_encrypted = acct.encrypted( 'password' )
7687
assert json.loads( json_encrypted ).get( 'address' ) == '336cbeab83accdb2541e43d514b62dc6c53675f4'
77-
88+
7889
acct_reco = Account( crypto='ETH' ).from_encrypted( json_encrypted, 'password' )
7990
assert acct_reco.address == '0x336cBeAB83aCCdb2541e43D514B62DC6C53675f4'
8091
assert acct.crypto == 'ETH'
@@ -97,7 +108,7 @@ def test_account_encrypt():
97108
).key.upper() == '09C2686880095B1A4C249EE3AC4EEA8A014F11E6F986D0B5025AC1F39AFBD9AE'
98109

99110
# This weird UTF-8 test I cannot get to pass, regardless of what format I supply the passphrase in..
100-
111+
101112
# acct_reco = Account( crypto='BTC' ).from_encrypted(
102113
# '6PRW5o9FLp4gJDDVqJQKJFTpMvdsSGJxMYHtHaQBF3ooa8mwD69bapcDQn',
103114
# bytes.fromhex('cf9300f0909080f09f92a9'), # '\u03D2\u0301\u0000\U00010400\U0001F4A9'

slip39/layout.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,8 @@ def layout_wallet(
354354
# We'll use the right side of the private region, each line rotated 90 degrees down and right.
355355
# So, we need the upper-left corner of the private-bg anchored at the upper-right corner of
356356
# private.
357-
private_length = private.y2 - private.y1 # line length is y-height
358-
private_fontsize = 6.8 # points == 1/72 inch
357+
private_length = private.y2 - private.y1 # line length is y-height
358+
private_fontsize = 6.8 # points == 1/72 inch
359359
private_height = private.x2 - private_qr.x2 - .05
360360
private_lineheight = private_fontsize / 72 / Text.SIZE_RATIO * .9 # in.
361361

@@ -370,12 +370,12 @@ def layout_wallet(
370370
)
371371
)
372372
# Now, add private key lines down the edge from right to left, rotating each into place
373-
for l in range( int( private_height // private_lineheight )):
373+
for ln in range( int( private_height // private_lineheight )):
374374
private.add_region(
375375
Text(
376-
f"private-{l}",
376+
f"private-{ln}",
377377
font = 'mono',
378-
x1 = private.x2 - private_lineheight * l,
378+
x1 = private.x2 - private_lineheight * ln,
379379
y1 = private.y1,
380380
x2 = private.x2 + private_length,
381381
y2 = private.y1 + private_lineheight,
@@ -686,7 +686,6 @@ def write_pdfs(
686686
wall_tpl['address-qr'] = public_qr.make_image( back_color="transparent" ).get_image()
687687
wall_tpl['address-qr-b'] = 'DEPOSIT/VERIFY'
688688

689-
690689
private_qr = qrcode.QRCode(
691690
version = None,
692691
error_correction = qrcode.constants.ERROR_CORRECT_M,
@@ -696,15 +695,16 @@ def write_pdfs(
696695
private_qr.add_data( private_enc )
697696

698697
wall_tpl['private-bg'] = os.path.join( images, '1x1-ffffff7f.png' )
698+
699699
def chunker( sequence, size ):
700700
while sequence:
701701
yield sequence[:size]
702702
sequence = sequence[size:]
703703

704704
# If not enough lines, will throw Exception, as it should! We don't want
705705
# to emit a Paper Wallet without the entire encrypted private key present.
706-
for l,line in enumerate( chunker( private_enc, 40 )):
707-
wall_tpl[f"private-{l}"]= line
706+
for ln,line in enumerate( chunker( private_enc, 40 )):
707+
wall_tpl[f"private-{ln}"] = line
708708
wall_tpl['private-hint-t'] = 'PASSPHRASE HINT:'
709709
wall_tpl['private-hint-bg'] = os.path.join( images, '1x1-ffffff7f.png' )
710710
wall_tpl['private-hint'] = wallet_pwd_hint

0 commit comments

Comments
 (0)