Skip to content

Commit

Permalink
Serialize with CBOR instead of pickle
Browse files Browse the repository at this point in the history
- I was having serious issues using pickle
- $ uvx hatch version rc
- updated uv handles dynamic project version in uv.lock
  • Loading branch information
eidorb committed Feb 14, 2025
1 parent 6c371ca commit 5523d3d
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 47 deletions.
14 changes: 4 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ $ pip install ubank
Register a new passkey with ubank:

```console
$ python -m ubank [email protected] --output passkey.pickle
$ python -m ubank [email protected] --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]
Expand All @@ -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.
Expand Down Expand Up @@ -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:

Expand All @@ -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:
Expand Down
44 changes: 22 additions & 22 deletions test_ubank.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


def test_version():
assert __version__ == "2.0.0rc0"
assert __version__ == "2.0.0rc1"


def test_int8array_to_bytes():
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(),
Expand All @@ -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.
Expand All @@ -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
25 changes: 11 additions & 14 deletions ubank.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import argparse
import json
import logging
import pickle
import uuid
from base64 import b64encode
from getpass import getpass
Expand All @@ -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 = {
Expand Down Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5523d3d

Please sign in to comment.