Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ 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`.
- Contains module and tests for Bech32/Bech32m and segwit addresses.

## 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
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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"]
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
bip32
pytest
14 changes: 5 additions & 9 deletions src/codex32/bip93.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
12 changes: 12 additions & 0 deletions tests/test_bip93.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -24,6 +26,7 @@
)
from codex32.bip93 import (
Codex32String,
IdNotLength4,
InvalidSeedLength,
MismatchedHrp,
MismatchedLength,
Expand Down Expand Up @@ -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"])
Expand Down
Loading