diff --git a/README.md b/README.md index 9ef17ce..e926e00 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ $ pip install ubank Register a new passkey with ubank: ```console -$ python -m ubank name@domain.com --output passkey.pickle +$ python -m ubank name@domain.com --output passkey.cbor Enter ubank password: Enter security code sent to 04xxxxx789: 123456 ``` -The above writes a new passkey to `passkey.pickle`. +The above writes a new passkey to `passkey.cbor`. You'll be prompted for your ubank username and SMS security code. > [!CAUTION] @@ -43,7 +43,7 @@ Use your passkey to access ubank's API in a Python script: from ubank import Client, Passkey # Load passkey from file. -with open("passkey.pickle", "rb") as f: +with open("passkey.cbor", "rb") as f: passkey = Passkey.load(f) # Authenticate to ubank with passkey and print account balances. @@ -125,7 +125,7 @@ Run tests locally: $ uv run pytest -v ``` -`test_ubank_client` requires a valid `passkey.pickle` file for testing ubank +`test_ubank_client` requires a valid `passkey.cbor` file for testing ubank authentication. Skip this test using the following expression: @@ -146,12 +146,6 @@ Old: 2.0.0 New: 2.1.0 ``` -Update uv lockfile: - -```console -$ uv lock -``` - Update `test_version` test. Create version tag and push to GitHub: diff --git a/test_ubank.py b/test_ubank.py index e28d1d1..46142a7 100644 --- a/test_ubank.py +++ b/test_ubank.py @@ -15,7 +15,7 @@ def test_version(): - assert __version__ == "2.0.0rc0" + assert __version__ == "2.0.0rc1" def test_int8array_to_bytes(): @@ -114,7 +114,7 @@ def test_serialize_assertion(): def test_passkey_serialization(tmp_path): - """Tests passkey pickle/unpickle.""" + """Tests passkey de/serialization.""" passkey = Passkey(passkey_name="test") # Passkey created with constructor should not have filename attribute. assert not hasattr(passkey, "filename") @@ -154,24 +154,24 @@ def test_passkey_serialization(tmp_path): passkey.device_id = "abc" passkey.username = "123" - # Dump and unpickle passkey. - with (tmp_path / "pickle").open("wb") as f: + # Serialize and deserialize passkey. + with (tmp_path / "passkey.cbor").open("wb") as f: passkey.dump(f) - with (tmp_path / "pickle").open("rb") as f: - unpickled_passkey = Passkey.load(f) + with (tmp_path / "passkey.cbor").open("rb") as f: + deserialized_passkey = Passkey.load(f) # Passkey created with Passkey.load() should have filename attribute. - assert hasattr(unpickled_passkey, "filename") + assert hasattr(deserialized_passkey, "filename") - assert unpickled_passkey.passkey_name == passkey.passkey_name - assert unpickled_passkey.hardware_id == passkey.hardware_id - assert unpickled_passkey.device_meta == passkey.device_meta - assert unpickled_passkey.device_id == passkey.device_id - assert unpickled_passkey.username == passkey.username + assert deserialized_passkey.passkey_name == passkey.passkey_name + assert deserialized_passkey.hardware_id == passkey.hardware_id + assert deserialized_passkey.device_meta == passkey.device_meta + assert deserialized_passkey.device_id == passkey.device_id + assert deserialized_passkey.username == passkey.username - assert unpickled_passkey.credential_id == passkey.credential_id - assert unpickled_passkey.private_key != passkey.private_key - assert unpickled_passkey.private_key.private_bytes( + assert deserialized_passkey.credential_id == passkey.credential_id + assert deserialized_passkey.private_key != passkey.private_key + assert deserialized_passkey.private_key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), @@ -180,16 +180,16 @@ def test_passkey_serialization(tmp_path): serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) - assert unpickled_passkey.aaguid == passkey.aaguid - assert unpickled_passkey.rp_id == passkey.rp_id - assert unpickled_passkey.user_handle == passkey.user_handle - assert unpickled_passkey.sign_count == passkey.sign_count + assert deserialized_passkey.aaguid == passkey.aaguid + assert deserialized_passkey.rp_id == passkey.rp_id + assert deserialized_passkey.user_handle == passkey.user_handle + assert deserialized_passkey.sign_count == passkey.sign_count def test_ubank_client(): """Tests Client using passkey loaded from file.""" # Load passkey from file. - with open("passkey.pickle", "rb") as f: + with open("passkey.cbor", "rb") as f: passkey = Passkey.load(f) # Authenticate to ubank with passkey. @@ -199,6 +199,6 @@ def test_ubank_client(): == "ubank" ) - # Updated passkey should be pickled automatically with updated sign_count. - with open("passkey.pickle", "rb") as f: + # Updated passkey should be serialized automatically with updated sign_count. + with open("passkey.cbor", "rb") as f: assert Passkey.load(f).sign_count == passkey.sign_count diff --git a/ubank.py b/ubank.py index 999cd2d..d183f8f 100644 --- a/ubank.py +++ b/ubank.py @@ -1,7 +1,6 @@ import argparse import json import logging -import pickle import uuid from base64 import b64encode from getpass import getpass @@ -11,7 +10,7 @@ import soft_webauthn from cryptography.hazmat.primitives import serialization -__version__ = "2.0.0rc0" +__version__ = "2.0.0rc1" # Unchanging headers in every request. base_headers = { @@ -173,31 +172,29 @@ def get(self, options, origin): # TODO: Replace if serialization PR merged https://github.com/bodik/soft-webauthn/pull/11 def dump(self, file: IO[bytes]): - """Writes pickled passkey to `file`.""" - serialized_passkey = Passkey(self.passkey_name) - for name, value in vars(self).items(): - setattr(serialized_passkey, name, value) - serialized_passkey.private_key = self.private_key.private_bytes( + """Serializes passkey to `file`.""" + passkey_dict = {name: value for name, value in vars(self).items()} + passkey_dict["private_key"] = self.private_key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) - pickle.dump(serialized_passkey, file) + file.write(soft_webauthn.cbor.dump_dict(passkey_dict)) # TODO: Replace if serialization PR merged https://github.com/bodik/soft-webauthn/pull/11 @classmethod def load(cls, file: IO[bytes]): - """Returns passkey unpickled from `file`.""" - serialized_passkey = pickle.load(file) - passkey = Passkey(serialized_passkey.passkey_name) - for name, value in vars(serialized_passkey).items(): + """Deserializes passkey from `file`.""" + passkey_dict = soft_webauthn.cbor.decode(file.read()) + passkey = Passkey(passkey_dict["passkey_name"]) + for name, value in passkey_dict.items(): setattr(passkey, name, value) passkey.private_key = serialization.load_pem_private_key( - serialized_passkey.private_key, + passkey.private_key, password=None, backend=soft_webauthn.default_backend(), ) - # Maintain reference to pickled passkey file. + # Maintain reference to serialized passkey filename. passkey.filename = file.name return passkey diff --git a/uv.lock b/uv.lock index ed30908..7b5ff38 100644 --- a/uv.lock +++ b/uv.lock @@ -245,7 +245,6 @@ wheels = [ [[package]] name = "ubank" -version = "2.0.0rc0" source = { editable = "." } dependencies = [ { name = "httpx" },