-
Notifications
You must be signed in to change notification settings - Fork 27
Multiwallet Feature: Generate Seed from Dice Rolls #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
from os import path | ||
from secrets import randbits | ||
from time import time | ||
from math import ceil | ||
|
||
from buidl.helper import big_endian_to_int, int_to_big_endian, sha256 | ||
|
||
|
@@ -98,6 +99,40 @@ def bytes_to_mnemonic(b, num_bits): | |
return " ".join(mnemonic) | ||
|
||
|
||
def dice_rolls_to_mnemonic(dice_rolls, num_words=24, allow_low_entropy=False): | ||
""" | ||
returns a mnemonic from a string of 6-sided dice rolls | ||
(>=100 rolls for 24 words recommended) | ||
(>=50 rolls for 12 words recommended) | ||
""" | ||
# check that the number of words provided is 12, 15, 18, 21, or 24 | ||
if num_words not in (12, 15, 18, 21, 24): | ||
raise InvalidBIP39Length( | ||
f"{num_words} words requested (must be 12, 15, 18, 21, or 24 words)" | ||
) | ||
# check that valid dice rolls have been provided | ||
if not isinstance(dice_rolls, str): | ||
raise ValueError("Dice rolls must be provided as a string") | ||
if ( | ||
len([roll for roll in dice_rolls if roll not in ("1", "2", "3", "4", "5", "6")]) | ||
> 0 | ||
): | ||
raise ValueError("Dice roll string contained invalid dice numbers") | ||
# entropy (in bits) per 6-sided dice roll (i.e. log2(6)=2.585) | ||
entropy_per_roll = 2.585 | ||
mnemonic_entropy = {12: 128, 15: 160, 18: 192, 21: 224, 24: 256} | ||
# check that the appropriate amount of entropy had been provided | ||
min_rolls_needed = ceil(mnemonic_entropy[num_words] / entropy_per_roll) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While technically correct (and great to see the code on how it's done), this would be easier to read for developers who are not cryptographically inclined if you included the calculation results in the Something like
|
||
if not allow_low_entropy and (len(dice_rolls) < min_rolls_needed): | ||
raise ValueError( | ||
f"Received {len(dice_rolls)} rolls but need at least {min_rolls_needed}" | ||
f" rolls (for {mnemonic_entropy[num_words]} bits of entropy)" | ||
) | ||
kept_bytes = mnemonic_entropy[num_words] // 8 | ||
rolls_hash = sha256(dice_rolls.encode())[:kept_bytes] | ||
return bytes_to_mnemonic(rolls_hash, kept_bytes * 8) | ||
|
||
|
||
class WordList: | ||
def __init__(self, filename, num_words): | ||
word_file = path.join(path.dirname(__file__), filename) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ | |
DEFAULT_P2WSH_PATH, | ||
) | ||
from buidl.libsec_status import is_libsec_enabled | ||
from buidl.mnemonic import BIP39 | ||
from buidl.mnemonic import BIP39, dice_rolls_to_mnemonic | ||
from buidl.shamir import ShareSet | ||
from buidl.psbt import MixedNetwork, PSBT | ||
|
||
|
@@ -246,6 +246,35 @@ def _get_bip39_firstwords(): | |
return fw | ||
|
||
|
||
##################################################################### | ||
# Dice rolls | ||
##################################################################### | ||
|
||
|
||
def _get_num_words(): | ||
num_words = _get_int( | ||
prompt="How many mnemonic words should be generated?", | ||
default=24, | ||
minimum=12, | ||
maximum=24, | ||
) | ||
if num_words not in (12, 15, 18, 21, 24): | ||
raise ValueError( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This entire method needs a |
||
"Provided number of mnemonic words must be 12, 15, 18, 21, or 24 words" | ||
) | ||
return num_words | ||
|
||
|
||
def _get_dice_values(): | ||
dice_vals = input( | ||
blue_fg( | ||
"Enter the numbers from the 6-sided dice rolls" | ||
'\n(e.g., "14625363...", >= 100 values recommended): ' | ||
) | ||
).strip() | ||
return dice_vals | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to handle bad entries (like someone entering a letter, space, a number not 1-6, etc) gracefully |
||
|
||
|
||
##################################################################### | ||
# PSBT Signer | ||
##################################################################### | ||
|
@@ -487,6 +516,55 @@ def do_generate_seed(self, arg): | |
print_yellow("Copy-paste this into Specter-Desktop:") | ||
print_green(key_record) | ||
|
||
def do_generate_seed_from_dice(self, arg): | ||
"""Calculate bitcoin public and private key information from BIP39 words created from dice rolls""" | ||
|
||
blue_fg("Generates a mnemonic from 6-sided dice rolls.") | ||
num_words = _get_num_words() | ||
dice_vals = _get_dice_values() | ||
|
||
if self.ADVANCED_MODE: | ||
password = _get_password() | ||
else: | ||
password = "" | ||
|
||
if _get_bool(prompt="Use Mainnet?", default=False): | ||
network = "mainnet" | ||
else: | ||
network = "testnet" | ||
|
||
if self.ADVANCED_MODE: | ||
path_to_use = _get_path(network=network) | ||
use_slip132_version_byte = _get_bool( | ||
prompt="Encode with SLIP132 version byte?", default=True | ||
) | ||
else: | ||
path_to_use = None # buidl will use default path | ||
use_slip132_version_byte = True | ||
|
||
words = dice_rolls_to_mnemonic(dice_vals, num_words) | ||
hd_priv = HDPrivateKey.from_mnemonic( | ||
mnemonic=words, | ||
password=password.encode(), | ||
network=network, | ||
) | ||
|
||
key_record = hd_priv.generate_p2wsh_key_record( | ||
bip32_path=path_to_use, use_slip132_version_byte=use_slip132_version_byte | ||
) | ||
|
||
print(yellow_fg("SECRET INFO") + red_fg(" (guard this VERY carefully)")) | ||
print_green( | ||
f"Dice rolls used: {dice_vals}" | ||
f"\nFull ({len(words.split())} word) mnemonic (including last word): {words}" | ||
) | ||
if password: | ||
print_green(f"Passphrase: {password}") | ||
|
||
print_yellow(f"\nPUBLIC KEY INFO ({network})") | ||
print_yellow("Copy-paste this into Specter-Desktop:") | ||
print_green(key_record) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has a lot of overlap with |
||
def do_create_output_descriptors(self, arg): | ||
"""Combine m-of-n public key records into a multisig output descriptor (account map)""" | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this is fine, but can be made simpler (and with a clearer error message) without the list comprehension.