Skip to content

Commit d42b411

Browse files
committed
fix(deps): bencode library
1 parent 992d798 commit d42b411

File tree

4 files changed

+89
-33
lines changed

4 files changed

+89
-33
lines changed

.github/workflows/unittests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
matrix:
1111
os: [macos-13, ubuntu-latest]
12-
python-version: ["3.8", "3.12"]
12+
python-version: ["3.9", "3.12"]
1313
bitcoind-version: ["29.2"]
1414

1515
steps:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "joinmarket"
77
version = "0.9.12dev"
88
description = "Joinmarket client library for Bitcoin coinjoins"
99
readme = "README.md"
10-
requires-python = ">=3.8,<3.13"
10+
requires-python = ">=3.9,<3.13"
1111
license = {file = "LICENSE"}
1212
dependencies = [
1313
"chromalog==1.0.5",
@@ -24,7 +24,7 @@ jmbitcoin = [
2424
jmclient = [
2525
"argon2_cffi==21.3.0",
2626
"autobahn==20.12.3",
27-
"bencoder.pyx==3.0.1",
27+
"fastbencode==0.3.5",
2828
"klein==20.6.0",
2929
"mnemonic==0.20",
3030
"pyjwt==2.4.0",

src/jmclient/storage.py

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
1+
import atexit
12
import os
23
import shutil
3-
import atexit
4-
import bencoder
54
from hashlib import sha256
5+
from typing import Any
6+
67
from argon2 import low_level
7-
from jmbase import aes_cbc_encrypt, aes_cbc_decrypt
8+
from fastbencode import bdecode, bencode_utf8
9+
10+
from jmbase import aes_cbc_decrypt, aes_cbc_encrypt
11+
812
from .support import get_random_bytes
913

1014

1115
class Argon2Hash(object):
12-
def __init__(self, password, salt=None, hash_len=32, salt_len=16,
13-
time_cost=500, memory_cost=1000, parallelism=4,
14-
argon2_type=low_level.Type.I, version=19):
16+
def __init__(
17+
self,
18+
password,
19+
salt=None,
20+
hash_len=32,
21+
salt_len=16,
22+
time_cost=500,
23+
memory_cost=1000,
24+
parallelism=4,
25+
argon2_type=low_level.Type.I,
26+
version=19,
27+
):
1528
"""
1629
args:
1730
password: password as bytes
@@ -30,11 +43,12 @@ def __init__(self, password, salt=None, hash_len=32, salt_len=16,
3043
'parallelism': parallelism,
3144
'hash_len': hash_len,
3245
'type': argon2_type,
33-
'version': version
46+
'version': version,
3447
}
3548
self.salt = salt if salt is not None else get_random_bytes(salt_len)
36-
self.hash = low_level.hash_secret_raw(password, self.salt,
37-
**self.settings)
49+
self.hash = low_level.hash_secret_raw(
50+
password, self.salt, **self.settings
51+
)
3852

3953

4054
class StorageError(Exception):
@@ -62,8 +76,9 @@ class Storage(object):
6276
6377
KDF: argon2, ENC: AES-256-CBC
6478
"""
79+
6580
MAGIC_UNENC = b'JMWALLET'
66-
MAGIC_ENC = b'JMENCWLT'
81+
MAGIC_ENC = b'JMENCWLT'
6782
MAGIC_DETECT_ENC = b'JMWALLET'
6883

6984
ENC_KEY_BYTES = 32 # AES-256
@@ -116,7 +131,10 @@ def was_changed(self):
116131
return self._data_checksum != self._get_data_checksum()
117132

118133
def check_password(self, password):
119-
return self._hash.hash == self._hash_password(password, self._hash.salt).hash
134+
return (
135+
self._hash.hash
136+
== self._hash_password(password, self._hash.salt).hash
137+
)
120138

121139
def change_password(self, password):
122140
if self.read_only:
@@ -128,7 +146,7 @@ def save(self):
128146
"""
129147
Write file to disk if data was modified
130148
"""
131-
#if not self.was_changed():
149+
# if not self.was_changed():
132150
# return
133151
if self.read_only:
134152
raise StorageError("Read-only storage cannot be saved.")
@@ -149,7 +167,7 @@ def _get_file_magic(cls, path):
149167
return fh.read(len(cls.MAGIC_ENC))
150168

151169
def _get_data_checksum(self):
152-
if self.data is None: #pragma: no cover
170+
if self.data is None: # pragma: no cover
153171
return None
154172
return sha256(self._serialize(self.data)).digest()
155173

@@ -167,7 +185,8 @@ def _set_hash(self, password):
167185
self._hash = self._hash_password(password)
168186

169187
def _save_file(self):
170-
assert self.read_only == False
188+
if self.read_only:
189+
raise StorageError("Read-only storage cannot be saved.")
171190
data = self._serialize(self.data)
172191
enc_data = self._encrypt_file(data)
173192

@@ -181,13 +200,17 @@ def _load_file(self, password):
181200
magic = data[:8]
182201

183202
if magic not in (self.MAGIC_ENC, self.MAGIC_UNENC):
184-
raise StorageError("File does not appear to be a joinmarket wallet.")
203+
raise StorageError(
204+
"File does not appear to be a joinmarket wallet."
205+
)
185206

186207
data = data[8:]
187208

188209
if magic == self.MAGIC_ENC:
189210
if password is None:
190-
raise RetryableStorageError("Password required to open wallet.")
211+
raise RetryableStorageError(
212+
"Password required to open wallet."
213+
)
191214
data = self._decrypt_file(password, data)
192215
else:
193216
assert magic == self.MAGIC_UNENC
@@ -211,7 +234,7 @@ def _write_file(self, data):
211234
shutil.copystat(self.path, tmpfile)
212235
fh.write(data)
213236

214-
#FIXME: behaviour with symlinks might be weird
237+
# FIXME: behaviour with symlinks might be weird
215238
shutil.move(tmpfile, self.path)
216239

217240
def _read_file(self):
@@ -223,12 +246,12 @@ def get_location(self):
223246
return self.path
224247

225248
@staticmethod
226-
def _serialize(data):
227-
return bencoder.bencode(data)
249+
def _serialize(data: Any) -> bytes:
250+
return bencode_utf8(data)
228251

229252
@staticmethod
230-
def _deserialize(data):
231-
return bencoder.bdecode(data)
253+
def _deserialize(data: bytes) -> Any:
254+
return bdecode(data)
232255

233256
def _encrypt_file(self, data):
234257
if not self.is_encrypted():
@@ -237,7 +260,7 @@ def _encrypt_file(self, data):
237260
iv = get_random_bytes(16)
238261
container = {
239262
b'enc': {b'salt': self._hash.salt, b'iv': iv},
240-
b'data': self._encrypt(data, iv)
263+
b'data': self._encrypt(data, iv),
241264
}
242265
return self._serialize(container)
243266

@@ -253,8 +276,9 @@ def _decrypt_file(self, password, data):
253276
return self._decrypt(container[b'data'], container[b'enc'][b'iv'])
254277

255278
def _encrypt(self, data: bytes, iv: bytes) -> bytes:
256-
return aes_cbc_encrypt(self._hash.hash,
257-
self.MAGIC_DETECT_ENC + data, iv)
279+
return aes_cbc_encrypt(
280+
self._hash.hash, self.MAGIC_DETECT_ENC + data, iv
281+
)
258282

259283
def _decrypt(self, data: bytes, iv: bytes) -> bytes:
260284
try:
@@ -265,12 +289,16 @@ def _decrypt(self, data: bytes, iv: bytes) -> bytes:
265289

266290
if not dec_data.startswith(self.MAGIC_DETECT_ENC):
267291
raise StoragePasswordError("Wrong password.")
268-
return dec_data[len(self.MAGIC_DETECT_ENC):]
292+
return dec_data[len(self.MAGIC_DETECT_ENC) :]
269293

270294
@classmethod
271295
def _hash_password(cls, password, salt=None):
272-
return Argon2Hash(password, salt,
273-
hash_len=cls.ENC_KEY_BYTES, salt_len=cls.SALT_LENGTH)
296+
return Argon2Hash(
297+
password,
298+
salt,
299+
hash_len=cls.ENC_KEY_BYTES,
300+
salt_len=cls.SALT_LENGTH,
301+
)
274302

275303
@staticmethod
276304
def _get_lock_filename(path: str) -> str:
@@ -296,8 +324,9 @@ def verify_lock(cls, path: str):
296324
raise RetryableStorageError(
297325
"File is currently in use (locked by pid {}). "
298326
"If this is a leftover from a crashed instance "
299-
"you need to remove the lock file `{}` manually.".
300-
format(locked_by_pid, cls._get_lock_filename(path))
327+
"you need to remove the lock file `{}` manually.".format(
328+
locked_by_pid, cls._get_lock_filename(path)
329+
)
301330
)
302331

303332
def _create_lock(self):

test/jmclient/test_storage.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from jmclient import storage
32
import pytest
43

@@ -81,6 +80,7 @@ def test_storage_invalid():
8180
MockStorage(b'garbagefile', __file__, b'password')
8281
pytest.fail("Non-wallet file, encrypted")
8382

83+
8484
def test_storage_readonly():
8585
s = MockStorage(None, 'nonexistant', b'password', create=True)
8686
s = MockStorage(s.file_data, __file__, b'password', read_only=True)
@@ -136,3 +136,30 @@ def test_storage_lock(tmpdir):
136136
s._create_lock()
137137
pytest.fail("It should not be possible to re-create a lock")
138138

139+
140+
testdata = {
141+
b"bytes_key": b"bytes_value",
142+
b"int_key": 42,
143+
b"list_key": [b"a", b"b", b"c", 1, 2, 3],
144+
b"dict_key": {b"nested": b"data", b"number": 999},
145+
}
146+
147+
148+
@pytest.mark.parametrize(
149+
"test_data, expected_out",
150+
[
151+
(testdata, testdata),
152+
({b"dict_key": {b"utf": "value"}}, {b"dict_key": {b"utf": b"value"}}),
153+
],
154+
)
155+
def test_bencode_roundtrip_consistency(test_data, expected_out):
156+
s = MockStorage(None, "nonexistant", None, create=True)
157+
158+
serialized1 = s._serialize(test_data)
159+
deserialized1 = s._deserialize(serialized1)
160+
161+
serialized2 = s._serialize(deserialized1)
162+
deserialized2 = s._deserialize(serialized2)
163+
164+
assert serialized1 == serialized2
165+
assert deserialized1 == deserialized2 == expected_out

0 commit comments

Comments
 (0)