Skip to content

Commit

Permalink
BIP-85 CLI supports different BIP-39 languages, 0.6.0 (#33)
Browse files Browse the repository at this point in the history
* longer timeout (for ci?) and fix message
* add whitespace per flake8 only on 3.12?
* lint on publish
  • Loading branch information
akarve authored Jun 5, 2024
1 parent 021233d commit 5304810
Show file tree
Hide file tree
Showing 9 changed files with 70 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: install
run: |
make install
make install-dev
make install-local
- name: check
run: make check
- name: test
Expand Down
24 changes: 15 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: all build check clean git-no-unsaved git-on-main got-off-main install install-dev
.PHONY: install-go lint publish push readme-cmds test test-network uninstall-dev
.PHONY: install-go install-local lint publish push readme-cmds test uninstall

build: clean download-wordlists check test
build: clean download-wordlists
python3 -m build

clean:
Expand All @@ -11,39 +11,45 @@ clean:
check:
black . --check
isort . --check
flake8 . --ignore=E501,W503

install:
pip install -r requirements.txt -r tst-requirements.txt
pip install -U bipsea

install-dev: uninstall-dev
install-local:
pip install -e .

install-dev:
pip install -r requirements.txt -r tst-requirements.txt

install-go:
# you must have go installed https://go.dev/doc/install
go install github.com/rhysd/actionlint/cmd/actionlint@latest
go install github.com/mrtazz/checkmake/cmd/checkmake@latest

uninstall-dev:
uninstall:
pip uninstall -y bipsea

lint:
isort .
black .
flake8 . --ignore=E501,W503
actionlint
flake8 . --ignore=E501,W503
checkmake Makefile

publish: build git-no-unsaved git-on-main
publish: install-local lint test readme-cmds build git-no-unsaved git-on-main
git pull origin main
python3 -m twine upload dist/*

push: lint check test git-off-main git-no-unsaved
push: lint test git-off-main git-no-unsaved
@branch=$$(git symbolic-ref --short HEAD); \
git push origin $$branch

test: readme-cmds
pytest -sx

test-publish: uninstall install readme-cmds

git-off-main:
@branch=$$(git symbolic-ref --short HEAD); \
if [ "$$branch" = "main" ]; then \
Expand Down Expand Up @@ -72,7 +78,7 @@ readme-cmds:
bipsea seed -t jpn -n 15
bipsea seed -f eng -u "airport letter idea forget broccoli prefer panda food delay struggle ridge salute above want dinner"
bipsea seed -f any -u "123456123456123456"
bipsea seed -f any -u "$$(cat README.md)"
bipsea seed -f any -u "$$(cat input.txt)"
bipsea seed | bipsea entropy
bipsea seed -f eng -u "load kitchen smooth mass blood happy kidney orbit used process lady sudden" | bipsea entropy -n 12
bipsea seed -f eng -u "load kitchen smooth mass blood happy kidney orbit used process lady sudden" | bipsea entropy -n 12 -i 1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ bipsea seed -f any -u "123456123456123456"
You can even load the input from a file.

```
bipsea seed -f any -u "$$(cat README.md)"
bipsea seed -f any -u "$$(cat input.txt)"
```

If you are now thinking, _I could use any string to derive a master key_,
Expand Down
1 change: 1 addition & 0 deletions input.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Satoshi Nakamoto published the Bitcoin white paper on October 31, 2008.
5 changes: 2 additions & 3 deletions src/bipsea/bip39.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@


def entropy_to_words(n_words: int, user_entropy: bytes, language: str):
"""If caller does not provide entropy use secrets.randbits
* Only produces seed words in English"""
"""If caller does not provide entropy use secrets.randbits"""
if n_words not in N_WORDS_ALLOWED:
raise ValueError(f"n_words must be one of {N_WORDS_ALLOWED}")

Expand All @@ -110,7 +109,7 @@ def entropy_to_words(n_words: int, user_entropy: bytes, language: str):
elif user_entropy and (difference <= -8):
warnings.warn(
(
f"Warning: {difference + n_entropy_bits} bits in, {n_entropy_bits} bits out."
f"{difference + n_entropy_bits} bits in, {n_entropy_bits} bits out."
" Input more entropy?"
)
)
Expand Down
2 changes: 1 addition & 1 deletion src/bipsea/bip85.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def apply_85(derived_key: ExtendedKey, path: str) -> Dict[str, Union[bytes, str]

if app == APPLICATIONS["words"]:
language_index, n_words = indexes[:2]
n_words = int(n_words[:-1]) # chop the ' from hardened derivation
n_words = int(n_words[:-1]) # chop ' from hardened derivation
if n_words not in N_WORDS_META.keys():
raise ValueError(f"Unsupported number of words: {n_words}.")
language = INDEX_TO_LANGUAGE[language_index]
Expand Down
60 changes: 39 additions & 21 deletions src/bipsea/bipsea.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .bip85 import (
APPLICATIONS,
DRNG,
INDEX_TO_LANGUAGE,
PURPOSE_CODES,
RANGES,
apply_85,
Expand All @@ -36,24 +37,26 @@
to_hex_string,
)

CODE_TO_LANG = {v["code"]: k for k, v in LANGUAGES.items()}
ISO_TO_LANGUAGE = {v["code"]: k for k, v in LANGUAGES.items()}


SEED_FROM_VALUES = [
"any",
"rand",
] + list(CODE_TO_LANG.keys())
] + list(ISO_TO_LANGUAGE.keys())


SEED_TO_VALUES = [
"tprv",
"xprv",
] + [code for code in CODE_TO_LANG.keys()]
] + list(ISO_TO_LANGUAGE.keys())

TIMEOUT = 0.1

ENTROPY_TO_VALUES = list(ISO_TO_LANGUAGE.keys())

N_WORDS_ALLOWED_STR = [str(n) for n in N_WORDS_ALLOWED]
N_WORDS_ALLOWED_HELP = "|".join(N_WORDS_ALLOWED_STR)

TIMEOUT = 0.1


logger = logging.getLogger(LOGGER)
Expand All @@ -70,14 +73,14 @@ def cli():
"-f",
"--from",
"from_",
type=click.Choice(SEED_FROM_VALUES, case_sensitive=True),
type=click.Choice(SEED_FROM_VALUES),
help="Input format.",
default="rand",
)
@click.option(
"-t",
"--to",
type=click.Choice(SEED_TO_VALUES, case_sensitive=True),
type=click.Choice(SEED_TO_VALUES),
default="xprv",
help="Output format.",
)
Expand All @@ -104,7 +107,7 @@ def bip39_cmd(from_, to, input, number, passphrase, pretty):
option_name="--from",
message="`--from words` requires `--input STRING`, `--from rand` forbids `--input`",
)
language = CODE_TO_LANG.get(from_)
language = ISO_TO_LANGUAGE.get(from_)
if language or from_ == "any":
if to == "words":
raise click.BadOptionUsage(
Expand All @@ -130,11 +133,11 @@ def bip39_cmd(from_, to, input, number, passphrase, pretty):
)
else: # from_ == rand
entropy = None
words = entropy_to_words(number, entropy, CODE_TO_LANG.get(to, "english"))
words = entropy_to_words(number, entropy, ISO_TO_LANGUAGE.get(to, "english"))

if to in CODE_TO_LANG.keys():
if to in ISO_TO_LANGUAGE.keys():
if pretty:
output = "\n".join(f"{i+1}) {w}" for i, w in enumerate(words))
output = "\n".join(f"{i + 1}) {w}" for i, w in enumerate(words))
else:
output = " ".join(words)

Expand Down Expand Up @@ -167,7 +170,7 @@ def bip39_cmd(from_, to, input, number, passphrase, pretty):
"--application",
default="words",
required=True,
type=click.Choice(APPLICATIONS.keys(), case_sensitive=True),
type=click.Choice(APPLICATIONS.keys()),
)
@click.option(
"-n",
Expand All @@ -192,9 +195,15 @@ def bip39_cmd(from_, to, input, number, passphrase, pretty):
@click.option(
"-u",
"--input",
help="`--input xprv123...` can be used instead of an input pipe `bipsea seed | bipsea entropy`",
help="alternative to a unix input pipe",
)
def bip85_cmd(application, number, index, special, input):
@click.option(
"-t",
"--to",
type=click.Choice(ENTROPY_TO_VALUES),
help="output language",
)
def bip85_cmd(application, number, index, special, input, to):
if not input:
stdin, _, _ = select.select([sys.stdin], [], [], TIMEOUT)
if stdin:
Expand Down Expand Up @@ -222,20 +231,29 @@ def bip85_cmd(application, number, index, special, input):
)
else:
number = 24

if not prv[:4] in ("tprv", "xprv"):
no_prv()

master = parse_ext_key(prv)

path = f"m/{PURPOSE_CODES['BIP-85']}"
app_code = APPLICATIONS[application]
path += f"/{app_code}"
if application == "words":
if number not in N_WORDS_ALLOWED:

if to:
if application != "words":
raise click.BadOptionUsage(
option_name="--number",
message=f"`--application wif` requires `--number NUMBER` in {N_WORDS_ALLOWED_HELP}",
option_name="--to",
message="--to requires `--application words`",
)
path += f"/0'/{number}'/{index}'"
else:
to = "eng"

if application == "words":
language = ISO_TO_LANGUAGE[to]
code_85 = next(i for i, l in INDEX_TO_LANGUAGE.items() if l == language)
path += f"/{code_85}/{number}'/{index}'"
elif application in ("wif", "xprv"):
path += f"/{index}'"
elif application in ("base64", "base85", "hex"):
Expand Down Expand Up @@ -275,8 +293,8 @@ def check_range(number: int, application: str):

def no_prv():
raise click.BadOptionUsage(
option_name="[incoming pipe]",
message="Bad input. Need xprv or tprv. Try `bipsea seed` | bipsea entropy`",
option_name="--input",
message="Missing xprv or tprv from pipe or --input. Try `bipsea seed | bipsea entropy`",
)


Expand Down
15 changes: 7 additions & 8 deletions src/bipsea/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from collections import Counter
from typing import List, Sequence

__version__ = "0.5.0"
__version__ = "0.6.0"
__app_name__ = "bipsea"

LOGGER = __app_name__
Expand Down Expand Up @@ -40,18 +40,17 @@ def shannon_entropy(input: List[str]) -> float:

def relative_entropy(input: Sequence, universe: set = ASCII_INPUTS) -> float:
input_set = set(list(input))
warn = False
if not input_set <= universe:
warnings.warn(
f"Unexpected input characters {input_set - universe}, expect bad entropy"
)
warn = True
overage = input_set - universe

ideal = math.log(len(universe), 2)
actual = shannon_entropy(input)
ratio = actual / ideal

if not warn:
if overage:
warnings.warn(
f"Some inputs outside (ASCII) universe: {overage}, can't estimate entropy"
)
else:
assert 0 < ratio <= 1.001

return ratio
Expand Down
6 changes: 3 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from data.bip85_vectors import BIP_39, HEX, PWD_BASE85, WIF

from bipsea.bip39 import LANGUAGES, verify_seed_words
from bipsea.bipsea import CODE_TO_LANG, N_WORDS_ALLOWED, cli
from bipsea.bipsea import ISO_TO_LANGUAGE, N_WORDS_ALLOWED, cli
from bipsea.util import ASCII_INPUTS, LOGGER

logger = logging.getLogger(LOGGER)
Expand Down Expand Up @@ -92,7 +92,7 @@ def test_seed_command_from_rand(runner, n, code):
assert len(words) == int(n)
assert result.exit_code == 0
if style != "--pretty":
assert verify_seed_words(words, CODE_TO_LANG[code])
assert verify_seed_words(words, ISO_TO_LANGUAGE[code])


def test_seed_command_from_custom_words(runner):
Expand Down Expand Up @@ -125,7 +125,7 @@ def test_seed_from_and_to_words(runner):
assert "--from rand" in result.output


@pytest.mark.parametrize("n", [-1, 11, 13, 0])
@pytest.mark.parametrize("n", [-1, 11, 13, 0, 25])
def test_seed_bad_n(runner, n):
result = runner.invoke(cli, ["seed", "--from", "eng", "-n", n])
assert result.exit_code != 0
Expand Down

0 comments on commit 5304810

Please sign in to comment.