Change: enforce entropy checks for temporary dice seeds
What changed, and why it matters
This commit tightens security for temporary Bitcoin wallet seeds created by rolling dice on a COLDCARD hardware wallet. Previously, temporary dice-generated seeds skipped the same mandatory entropy checks and warnings used for permanent dice-generated seeds. The change makes temporary dice seeds follow the same rules, so users cannot accidentally create a weak wallet from too few dice rolls. This is a defensive hardening fix, not an active bug that steals funds, but weak entropy could let an attacker guess a wallet's private keys.
Treat as a security hardening improvement and include in release notes. No immediate incident response is required, but users who created temporary dice seeds with very few rolls in the past should consider those seeds lower-entropy and migrate funds to a freshly generated seed.
Security signals we found
Hardening: applies existing entropy enforcement to an additional code path
Weak-entropy reduction: prevents low-entropy temporary dice seeds
UX warning added for dice-only temporary seeds
Removal of duplicated seed-generation function
Test coverage added for rejected under-rolled ephemeral dice seeds
Evidence from the diff
The patch removes the separate ephemeral_seed_generate_from_dice() path and routes temporary (ephemeral) dice seed creation through new_from_dice() with an ephemeral flag. That path already enforces minimum dice-roll counts (enforce=True) and shows a warning that dice-only seeds contain no hardware randomness and that the final hash must be kept secret. The old ephemeral path called add_dice_rolls() without enforce=True, so it did not enforce the 50-roll/99-roll minimums for 12- and 24-word seeds. Tests now verify the warning and the ‘Not enough dice rolls’ rejection for ephemeral dice seeds.
Changed components
shared/seed.pyCOLDCARD temporary/ephemeral seed generationDice-roll entropy collection UI flowInspect captured patch +59 / −28
### releases/Next-ChangeLog.md
@@ -24,6 +24,8 @@ This lists the new changes that have not yet been published in a normal release.
continue mashing beyond 65 presses to contribute additional timing entropy.
- Enhancement: Dice-only seed generation now warns that no hardware randomness is
included and the final hash shown on-screen must be kept secret.
+- Change: Temporary dice-only seeds now use the same warning and mandatory
+ entropy checks as master dice-only seeds.
- Change: Generated Temporary Seeds and generated CCC key C now require extra
user supplied entropy.
- Bugfix: Detect RNG_SR_SEIS and RNG_SR_SECS, retry safely, and fail closed on persistent faults.
### shared/seed.py
@@ -520,7 +520,7 @@ async def add_dice_rolls(count, seed, judge_them, nwords=None, enforce=False):
return count, seed
-async def new_from_dice(nwords):
+async def new_from_dice(nwords, ephemeral=False):
# Use lots of (D6) dice rolls to create seed entropy.
# Note: only 2.585 bits of entropy per roll, so need lots!
# 50 => 128bits, 99 => 256bits
@@ -535,9 +535,13 @@ async def new_from_dice(nwords):
count, seed = await add_dice_rolls(count, seed, True, nwords, enforce=True)
if count == 0: return
- words = await approve_word_list(seed, nwords)
+ words = await approve_word_list(seed, nwords, ephemeral=ephemeral)
if words:
- await commit_new_words(words)
+ if ephemeral:
+ dis.fullscreen("Applying...")
+ await set_ephemeral_seed_words(words, origin='Dice')
+ else:
+ await commit_new_words(words)
def in_seed_vault(encoded):
# Test if indicated secret is in the seed vault already.
@@ -639,22 +643,6 @@ async def set_ephemeral_seed_words(words, origin):
await set_ephemeral_seed(encoded, origin=origin)
goto_top_menu()
-async def ephemeral_seed_generate_from_dice(nwords):
- # Use lots of (D6) dice rolls to create seed entropy.
- # Note: only 2.585 bits of entropy per roll, so need lots!
- # 50 => 128bits, 99 => 256bits
-
- seed = b''
- count = 0
-
- count, seed = await add_dice_rolls(count, seed, True, nwords)
- if count == 0: return
-
- words = await approve_word_list(seed, nwords, ephemeral=True)
- if words:
- dis.fullscreen("Applying...")
- await set_ephemeral_seed_words(words, origin='Dice')
-
def generate_seed():
# Generate 32 bytes of best-quality high entropy from independent sources.
import callgate
@@ -1401,7 +1389,7 @@ async def ephemeral_seed_generate(menu, label, item):
@staticmethod
async def ephemeral_seed_generate_from_dice(menu, label, item):
- return await ephemeral_seed_generate_from_dice(item.arg)
+ return await new_from_dice(item.arg, ephemeral=True)
@classmethod
def construct(cls):
### testing/test_ephemeral.py
@@ -382,14 +382,23 @@ def doit(num_words, dice=False, from_main=False, seed_vault=None, testnet=True):
ephemeral_seed_disabled_ui()
pick_menu_item("Generate Words")
- if not dice:
+ if dice:
+ pick_menu_item(f"{num_words} Word Dice Roll")
+ time.sleep(0.1)
+ title, _ = cap_story()
+ assert title == 'WARNING'
+ press_select() # acknowledge dice-only warning
+ time.sleep(0.1)
+
+ num_rolls = 50 if num_words == 12 else 99
+ for i in range(num_rolls):
+ need_keypress(str((i % 6) + 1))
+ time.sleep(0.01)
+ press_select()
+ else:
pick_menu_item(f"{num_words} Words")
enter_mash_entropy()
time.sleep(0.1)
- else:
- pick_menu_item(f"{num_words} Word Dice Roll")
- for ch in '123456\r\r':
- need_keypress(ch)
time.sleep(0.2)
title, story = cap_story()
@@ -493,7 +502,7 @@ def doit(way, extended_key=None, testnet=True, seed_vault=False, from_main=False
@pytest.mark.parametrize("num_words", [12, 24])
-@pytest.mark.parametrize("dice", [False, True])
+@pytest.mark.parametrize("dice", [False, True], ids=["generated", "dice"])
@pytest.mark.parametrize("seed_vault", [False, True])
@pytest.mark.parametrize("preserve_settings", [False, True])
def test_ephemeral_seed_generate(num_words, generate_ephemeral_words, dice,
@@ -514,6 +523,38 @@ def test_ephemeral_seed_generate(num_words, generate_ephemeral_words, dice,
restore_main_seed(preserve_settings)
+@pytest.mark.parametrize("num_words,min_rolls", [(12, 50), (24, 99)])
+def test_ephemeral_dice_security_checks(reset_seed_words, goto_eph_seed_menu,
+ ephemeral_seed_disabled, pick_menu_item,
+ cap_story, press_select, press_cancel,
+ need_keypress, num_words, min_rolls):
+ reset_seed_words()
+ goto_eph_seed_menu()
+ ephemeral_seed_disabled()
+
+ pick_menu_item("Generate Words")
+ pick_menu_item(f"{num_words} Word Dice Roll")
+
+ title, warning = cap_story()
+ assert title == 'WARNING'
+ assert 'only source of randomness' in warning
+ assert 'final hash can recreate your wallet' in warning
+ press_select()
+
+ for ch in '123456':
+ need_keypress(ch)
+ press_select()
+ time.sleep(0.1)
+
+ _, story = cap_story()
+ assert 'Not enough dice rolls' in story
+ assert f'minimum for {num_words} word seeds' in story
+ assert f'need at least {min_rolls} rolls' in story
+ press_cancel()
+ time.sleep(0.1)
+ ephemeral_seed_disabled()
+
+
def test_ephemeral_seed_import_qr_bad_checksum(reset_seed_words, goto_eph_seed_menu,
pick_menu_item, scan_a_qr, cap_story,
press_cancel, is_q1):
@@ -1729,8 +1770,8 @@ def test_seed_vault_enable_on_tmp(generate_ephemeral_words, reset_seed_words,
settings_remove("seeds")
goto_eph_seed_menu()
ephemeral_seed_disabled()
- e_seed_words = generate_ephemeral_words(num_words=12, dice=False,
- from_main=True, seed_vault=False)
+ e_seed_words = generate_ephemeral_words(num_words=12, from_main=True,
+ seed_vault=False)
verify_ephemeral_secret_ui(mnemonic=e_seed_words, seed_vault=False)
goto_home()
pick_menu_item("Advanced/Tools")Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.