Visualize WIF with ability to import to WIF store
What changed, and why it matters
This commit adds a feature to COLDCARD Q1 hardware wallets that lets users scan a QR code containing a private key (in WIF format), view its details, and optionally save it to the device's WIF Store. The change itself is a user-facing feature addition, not a bug fix. There is no direct evidence in the commit or supplied references that this is a security patch or addresses a vulnerability. However, any feature that imports and stores private keys carries inherent security and usability risks, such as users importing untrusted or leaked keys, or accidentally exposing keys via QR codes.
Treat as a feature commit, not a vulnerability patch. If auditing, review whether the WIF Store is protected by the device's seed/login security model, whether imported keys are clearly labeled as non-master/non-derived, and whether the QR import path could be triggered by malicious QR codes without sufficient user confirmation. No immediate patching is indicated by the diff alone.
Security signals we found
Feature adds persistent storage of externally supplied private keys
Private key material displayed on screen and imported based on user button press
No passphrase or authentication step visible before saving private key
WIF Store already existed; this commit only adds a new import path via QR scan
No commit message or vendor reference describes this as a security fix
Evidence from the diff
The commit moves ux_visualize_wif() from shared/ux_q1.py to shared/wif.py and extends it. Previously, scanning a WIF QR code only displayed the WIF string, chain, private key hex, and public key SEC. Now, if the WIF is compressed and its testnet/mainnet flag matches the current chain, the UI offers to import the key into the persistent WIF Store (settings key ‘wifs’) by pressing ‘1’. The store is saved to device settings. Duplicate imports are rejected. Tests cover mainnet/testnet and compressed/uncompressed WIF variants, verifying import eligibility and duplicate detection.
Changed components
shared/ux_q1.pyshared/wif.pyCOLDCARD Q1 QR scanner WIF import flowDevice WIF Store (settings key 'wifs')Inspect captured patch +91 / −8
diff --git a/shared/ux_q1.py b/shared/ux_q1.py
index 9f763ea..35bdb72 100644
--- a/shared/ux_q1.py
+++ b/shared/ux_q1.py
@@ -1000,6 +1000,7 @@ class QRScannerInteraction:
elif what == "wif":
data, = vals
wif_str, key_pair, compressed, testnet = data
+ from wif import ux_visualize_wif
await ux_visualize_wif(wif_str, key_pair, compressed, testnet)
elif what == "vmsg":
@@ -1140,14 +1141,6 @@ async def ux_visualize_bip21(proto, addr, args):
from ownership import OWNERSHIP
await OWNERSHIP.search_ux(addr, args)
-async def ux_visualize_wif(wif_str, kp, compressed, testnet):
- # TODO: remove until we support signing w/ WIF keys IMHO
- from ux import ux_show_story
- msg = wif_str + "\n\n"
- msg += "chain: %s\n\n" % ("XTN" if testnet else "BTC")
- msg += "private key hex:\n" + b2a_hex(kp.privkey()).decode() + "\n\n"
- msg += "public key sec:\n" + b2a_hex(kp.pubkey().to_bytes(not compressed)).decode() + "\n\n"
- await ux_show_story(msg, title="WIF")
async def qr_msg_sign_done(signature, address, text):
from ux import ux_show_story
diff --git a/shared/wif.py b/shared/wif.py
index ca131ce..271c934 100644
--- a/shared/wif.py
+++ b/shared/wif.py
@@ -44,6 +44,30 @@ def iter_wif_store_addresses(chain, addr_fmt):
yield i, chain.address(node, addr_fmt)
+async def ux_visualize_wif(wif_str, kp, compressed, testnet):
+ ch_str = ("XTN" if testnet else "BTC")
+ sk = b2a_hex(kp.privkey()).decode()
+ pk = b2a_hex(kp.pubkey().to_bytes(not compressed)).decode()
+ msg = "%s\n\nchain: %s\n\nPrivkey:\n%s\n\nPubkey:\n%s" % (wif_str, ch_str, sk, pk)
+ esc = ""
+ if compressed and (testnet == (chains.current_chain().ctype != "BTC")):
+ # we only support compressed in WIF store
+ msg += "\n\nPress (1) to import to WIF Store."
+ esc += "1"
+
+ ch = await ux_show_story(msg, title="WIF", escape=esc)
+ if ch == "1":
+ saved = settings.get("wifs", [])
+ if (pk, sk) in saved:
+ await ux_show_story("Already saved in WIF Store.", title="Failure")
+ return
+
+ saved.append((pk, sk))
+ settings.set('wifs', saved)
+ settings.save()
+ await ux_show_story("Saved to WIF Store.", title="Success")
+
+
class WIFStore(MenuSystem):
MAX_ITEMS = 30
diff --git a/testing/test_wif.py b/testing/test_wif.py
index 8f750ae..7b2e49e 100644
--- a/testing/test_wif.py
+++ b/testing/test_wif.py
@@ -738,4 +738,70 @@ def test_wif_store_signing_with_master(fake_txn, start_sign, end_sign, cap_story
end_sign(finalize=True)
+@pytest.mark.parametrize("wif", [
+ "KwYP78wzyiuShCqppuh1JZQCnKtFdAaY6HcDhRmhDy21vGSiF37N", # mainnet compressed
+ "5JwcuSWKH4PqV1mU8JSK9BBUkLjuAUS3MFHfP1w1qy9HjnXpavk", # mainnet uncompressed
+ "91cLPdroy4CtRYxWBXxgggqNnZrTz2CoJrLDkjDjcnkMP74gX5S", # testnet uncompressed
+ "cUR6JLQCmdPPt3op4jEYmFhjHpWC2AoZaWmZqoDaBQYMXN4QeKuc", # testnet compressed
+])
+@pytest.mark.parametrize("testnet", [True, False])
+def test_visualize_wif(wif, testnet, is_q1, goto_home, need_keypress, use_testnet, use_mainnet,
+ scan_a_qr, cap_story, settings_remove, press_select):
+ if not is_q1:
+ raise pytest.skip("need scanner")
+
+ settings_remove("wifs")
+
+ if testnet:
+ use_testnet()
+ else:
+ use_mainnet()
+
+ goto_home()
+ need_keypress(KEY_QR)
+ scan_a_qr(wif)
+ time.sleep(1)
+ title, story = cap_story()
+ split_story = story.split("\n\n")
+ pubkey = split_story[3].split("\n")[-1]
+ if wif[0] in "59":
+ # uncompressed
+ assert pubkey[0:2] == "04"
+ assert len(pubkey) == 130
+ else:
+ # compressed
+ assert pubkey[0:2] in ["02", "03"]
+ assert len(pubkey) == 66
+
+ if testnet:
+ # we are on testnet, mainnet keys are not importable
+ if wif[0] in "K59":
+ assert "Press (1) to import to WIF Store" not in story
+ return
+ else:
+ # we are on mainnet, testnet keys are not importable
+ if wif[0] in "c59":
+ assert "Press (1) to import to WIF Store" not in story
+ return
+
+ assert "Press (1) to import to WIF Store" in story
+ need_keypress("1")
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "Success"
+ assert "Saved to WIF Store" in story
+ press_select()
+
+ # try import same wif
+ goto_home()
+ need_keypress(KEY_QR)
+ scan_a_qr(wif)
+ time.sleep(1)
+ need_keypress("1")
+ time.sleep(.1)
+ title, story = cap_story()
+ assert title == "Failure"
+ assert "Already saved in WIF Store" in story
+ press_select()
+
# EOF
\ No newline at end of file
Why this scored 29/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.