diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 96b1dda..5840f5f 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -9,14 +9,16 @@ jobs: build: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} + allow-prereleases: ${{ matrix.python-version == '3.15' }} - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8ed5f06..95fa4f0 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -19,14 +19,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} + allow-prereleases: ${{ matrix.python-version == '3.15' }} - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/README.md b/README.md index 3507281..1aae028 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Shamir secret sharing scheme (SSSS) interpolation helpers and helpers to build c - Regular checksum (13 chars) and long checksum (15 chars) support. - Construct codex32 strings from raw seed bytes via `from_seed`. - CRC-based default padding scheme for `from_seed`. -- Default `from_seed` identifier is the bech32-encoded BIP32 fingerprint. +- `from_seed` requires an explicit four-character identifier in its prefix. - Interpolate/recover shares via `interpolate_at`. - Parse codex32 strings and access parts via properties. - Mutate codex32 strings by reassigning `is_upper`, `hrp`, `k`, `ident`, `share_idx`, `data`, and `pad_val`. @@ -21,9 +21,11 @@ Shamir secret sharing scheme (SSSS) interpolation helpers and helpers to build c ## Security Caution: This is reference code. Verify carefully before using with real funds. +For wallet backups, obtain the identifier from a trusted wallet record; +`from_seed` does not derive or verify a BIP32 fingerprint. ## Installation -**Compatibility:** Python 3.10–3.14 +**Compatibility:** Python 3.10–3.15 **Recommended:** use a virtual environment ### Linux / macOS @@ -47,7 +49,7 @@ from codex32 import Codex32String # Create from seed bytes s = Codex32String.from_seed( bytes.fromhex('ffeeddccbbaa99887766554433221100'), - "ms13cashs", # prefix string, (HRP + '1' + header) + "ms13cashs", # prefix string with explicit identifier (HRP + '1' + header) 0 # padding value (default "CRC", otherwise integer) ) print(s.s) # codex32 string diff --git a/pyproject.toml b/pyproject.toml index a2c4094..6ad3372 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ ] description = "Python reference implementation for codex32 (BIP93) and codex32-encoded master seeds." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.10,<3.16" license = "MIT" license-files = ["LICENSE*"] maintainers = [ @@ -26,11 +26,13 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Operating System :: OS Independent", "Topic :: Security :: Cryptography", "Topic :: Software Development :: Libraries", ] -dependencies = ["bip32>=5.0.0"] +dependencies = [] [project.optional-dependencies] dev = ["pytest", "flake8", "mypy", "black", "isort", "ruff"] diff --git a/requirements.txt b/requirements.txt index 68fb66e..e079f8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -bip32 pytest diff --git a/src/codex32/bip93.py b/src/codex32/bip93.py index d36b42e..4fb4234 100644 --- a/src/codex32/bip93.py +++ b/src/codex32/bip93.py @@ -27,8 +27,6 @@ """Reference implementation for codex32/Long codex32 and codex32-encoded master seeds.""" -from bip32 import BIP32 - from codex32.bech32 import ( CHARSET, chars_to_u5, @@ -263,16 +261,14 @@ def interpolate_at( @classmethod def from_seed( - cls, data: bytes, prefix: str = "ms10", pad_val: int | str = "CRC" + cls, data: bytes, prefix: str, pad_val: int | str = "CRC" ) -> "Codex32String": - """Create Codex32String given prefix and bare seed data.""" + """Create Codex32String from seed bytes and a prefix with an identifier.""" hrp, data_part = u5_parse(prefix) header = u5_to_chars(data_part) k = "0" if not header else header[:1] - if not (ident := header[1 : max(5, len(header) - 1)]): - bip32_fingerprint = BIP32.from_seed(data).get_fingerprint() - ident = u5_to_chars(convertbits(bip32_fingerprint, 8, 5)[:4]) - elif len(ident) != 4: - raise IdNotLength4(f"identifier had wrong length {len(ident)}") + ident = header[1 : max(5, len(header) - 1)] + if len(ident) != 4: + raise IdNotLength4(f"prefix must contain a four-character identifier, got {len(ident)}") share_idx = "s" if not header[5:] else header[5:6] return cls(encode(hrp, k + ident + share_idx, data, pad_val)) diff --git a/tests/test_bip93.py b/tests/test_bip93.py index 8a5d15f..f466fec 100644 --- a/tests/test_bip93.py +++ b/tests/test_bip93.py @@ -1,5 +1,7 @@ # tests/test_bip93.py """Tests for BIP-93 codex32 implementation.""" +import inspect + import pytest from data.bip93_vectors import ( VECTOR_1, @@ -24,6 +26,7 @@ ) from codex32.bip93 import ( Codex32String, + IdNotLength4, InvalidSeedLength, MismatchedHrp, MismatchedLength, @@ -59,6 +62,15 @@ def test_parts(): assert s.data.hex() == VECTOR_1["secret_hex"] +def test_from_seed_requires_explicit_identifier(): + """Seed encoding must not infer an identifier or import BIP32.""" + seed = bytes.fromhex(VECTOR_3["secret_hex"]) + prefix = inspect.signature(Codex32String.from_seed).parameters["prefix"] + assert prefix.default is inspect.Parameter.empty + with pytest.raises(IdNotLength4, match="four-character identifier"): + Codex32String.from_seed(seed, "ms10") + + def test_derive_and_recover(): """Test Vector 2: derive new share and recover the secret""" a = Codex32String(VECTOR_2["share_A"])