From bb54af05df7a76f5c99daa25a9107cffd19208ec Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 20 Aug 2021 20:59:11 -0500 Subject: [PATCH] chaining camera entropy; mnemonic methods factored out; initial tests --- src/seedsigner/helpers/mnemonic_generation.py | 44 ++++++++++ src/seedsigner/views/seed_tools_view.py | 80 +++++++++++-------- tests/test_mnemonic_generation.py | 32 ++++++++ 3 files changed, 121 insertions(+), 35 deletions(-) create mode 100644 src/seedsigner/helpers/mnemonic_generation.py create mode 100644 tests/test_mnemonic_generation.py diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py new file mode 100644 index 00000000..4a1c78cf --- /dev/null +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -0,0 +1,44 @@ +from embit import bip39 +from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes + + + +def calculate_checksum(partial_mnemonic: list): + # Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum + if len(partial_mnemonic) not in [11, 23]: + raise Exception("Pass in a 11- or 23-word mnemonic") + + # Work on a copy of the input list + mnemonic_copy = partial_mnemonic.copy() + mnemonic_copy.append("abandon") + + # Ignores the final checksum word and recalcs + mnemonic_bytes = bip39.mnemonic_to_bytes(" ".join(mnemonic_copy), ignore_checksum=True) + + # Return as a list + return bip39.mnemonic_from_bytes(mnemonic_bytes).split() + + + +def generate_mnemonic_from_bytes(entropy_bytes): + # Return as a list + return bip39.mnemonic_from_bytes(entropy_bytes).split() + + + +def generate_mnemonic_from_dice(roll_data: str): + entropyinteger = int(roll_data, 6) + entropy_bytes = entropyinteger.to_bytes(32, byteorder="little") + + # Return as a list + return bip39.mnemonic_from_bytes(entropy_bytes).split() + + + +# Note: This currently isn't being used since we're now chaining hashed bytes for the +# image-based entropy and aren't just ingesting a single image. +def generate_mnemonic_from_image(image): + hash = hashlib.sha256(image.tobytes()) + + # Return as a list + return bip39.mnemonic_from_bytes(hash.digest()).split() diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 179b3e3b..09fcf998 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -5,11 +5,12 @@ from PIL import ImageDraw, Image from PIL.ImageOps import autocontrast import hashlib import math +import os import time # Internal file class dependencies from . import View -from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay +from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay, mnemonic_generation from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR @@ -665,11 +666,8 @@ class SeedToolsView(View): ### def display_last_word(self, partial_seed_phrase) -> list: - stringphrase = " ".join(partial_seed_phrase).strip() + " abandon" - bytes = mnemonic_to_bytes(stringphrase, ignore_checksum=True) - finalseed = mnemonic_from_bytes(bytes) - splitseed = finalseed.split() - last_word = splitseed[-1] + finalseed = mnemonic_generation.calculate_checksum(partial_seed_phrase) + last_word = finalseed[-1] self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) tw, th = self.draw.textsize("The final word is :", font=View.IMPACT23) @@ -683,7 +681,7 @@ class SeedToolsView(View): View.DispShowImage() input = self.buttons.wait_for([B.KEY_RIGHT]) - return splitseed[:] + return finalseed ### ### Display Seed from Dice @@ -715,7 +713,7 @@ class SeedToolsView(View): return [] if self.roll_number >= 100: - self.dice_seed_phrase = self.calc_seed_from_dice() + self.dice_seed_phrase = mnemonic_generation.generate_mnemonic_from_dice(self.roll_data) return self.dice_seed_phrase[:] def dice_arrow_up(self): @@ -796,11 +794,13 @@ class SeedToolsView(View): return True def dice_arrow_press(self): - self.roll_number = self.roll_number + 1 + self.roll_number += 1 if self.dice_selected == 6: - self.roll_data = self.roll_data + str(0).strip() + self.roll_data += "0" else: - self.roll_data = self.roll_data + str(self.dice_selected).strip() + self.roll_data += str(self.dice_selected) + + # Reset for the next UI render self.dice_selected = 5 if self.roll_number < 100: self.draw_dice(self.dice_selected) @@ -899,19 +899,6 @@ class SeedToolsView(View): self.dice_selected = dice_selected - def calc_seed_from_dice(self): - entropyinteger = int(self.roll_data, 6) - entropyinbytes = entropyinteger.to_bytes(32, byteorder="little") - badseedphrase_str = mnemonic_from_bytes(entropyinbytes) - badseedphrase_list = badseedphrase_str.split() - badseedphrase_list.pop(-1) - calclastwordphrasestr = " ".join(badseedphrase_list) + " abandon" - goodphrasebytes = mnemonic_to_bytes(calclastwordphrasestr, ignore_checksum=True) - goodseedphrasestr = mnemonic_from_bytes(goodphrasebytes) - seed_phrase = goodseedphrasestr.split() - - return seed_phrase - ### ### Display Seed Phrase ### @@ -1187,10 +1174,16 @@ class SeedToolsView(View): self.controller.menu_view.draw_modal(["Initializing Camera..."]) self.controller.camera.start_video_stream_mode(resolution=(240, 240), framerate=24, format="rgb") + # save preview image frames to use as additional entropy below + preview_images = [] + max_entropy_frames = 50 + while True: frame = self.controller.camera.read_video_stream(as_image=True) if frame is not None: View.DispShowImageWithText(frame, "click joystick", text_color=View.color, text_background=(0,0,0,225)) + if len(preview_images) < max_entropy_frames: + preview_images.append(frame) if self.buttons.check_for_low(B.KEY_LEFT): self.words = [] @@ -1207,8 +1200,8 @@ class SeedToolsView(View): self.controller.camera.stop_single_frame_mode() # Prep a copy of the image for display. The actual image data is 720x480 - # Present just a center crop to fit the screen and to keep some of the - # data hidden. + # Present just a center crop and resize it to fit the screen and to keep some of + # the data hidden. display_version = autocontrast( seed_entropy_image, cutoff=2 @@ -1233,18 +1226,35 @@ class SeedToolsView(View): reshoot = True else: - # TODO: Pull this out into its own method so we can write tests against it - hash = hashlib.sha256(seed_entropy_image.tobytes()) - badseedphrase_str = mnemonic_from_bytes(hash.digest()) - badseedphrase_list = badseedphrase_str.split() - badseedphrase_list.pop(-1) - calclastwordphrasestr = " ".join(badseedphrase_list) + " abandon" - goodphrasebytes = mnemonic_to_bytes(calclastwordphrasestr, ignore_checksum=True) - goodseedphrasestr = mnemonic_from_bytes(goodphrasebytes) - self.words = goodseedphrasestr.split() + # Build in some hardware-level uniqueness via CPU unique Serial num + try: + stream = os.popen("cat /proc/cpuinfo | grep Serial") + output = stream.read() + serial_num = output.split(":")[-1].strip().encode('utf-8') + serial_hash = hashlib.sha256(serial_num) + hash_bytes = serial_hash.digest() + except Exception as e: + print(repr(e)) + hash_bytes = b'0' + + # Build in modest entropy via millis since power on + millis_hash = hashlib.sha256(hash_bytes + str(time.time()).encode('utf-8')) + hash_bytes = millis_hash.digest() + + # Build in better entropy by chaining the preview frames + for frame in preview_images: + img_hash = hashlib.sha256(hash_bytes + frame.tobytes()) + hash_bytes = img_hash.digest() + + # Finally build in our headline entropy via the new full-res image + final_hash = hashlib.sha256(hash_bytes + seed_entropy_image.tobytes()) + self.words = mnemonic_generation.generate_mnemonic_from_bytes(final_hash.digest()) # Image should never get saved nor stick around in memory seed_entropy_image = None + preview_images = None + final_hash = None + hash_bytes = None # self.buttons.trigger_override(True) return (reshoot, self.words) diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py new file mode 100644 index 00000000..65f002a8 --- /dev/null +++ b/tests/test_mnemonic_generation.py @@ -0,0 +1,32 @@ +import random + +from embit import bip39 +from seedsigner.helpers import mnemonic_generation + + + +def test_dice_rolls(): + """ Given 99 random dice rolls, the resulting mnemonic should be valid. """ + dice_rolls = "" + for i in range(0, 99): + # Do not need truly rigorous random for this test + dice_rolls += str(random.randint(0, 5)) + + mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls) + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + + + +def test_calculate_checksum(): + """ Given an 11-word or 23-word mnemonic, the calculated checksum should yield a + valid complete mnemonic. + """ + # Test mnemonics from https://iancoleman.io/bip39/ + partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + + partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) +