refactor: optimization on usage of foundation-ur and urtypes libs (#825)
What changed, and why it matters
This commit is a code cleanup that changes how the Krux firmware imports and uses two libraries for handling Uniform Resource (UR) QR codes. It replaces broad 'import urtypes' statements with specific imports of individual modules, normalizes UR type string comparisons to uppercase, and simplifies one QR parser result return. There is no direct evidence in the commit that this fixes a security vulnerability.
No security action required. Treat as ordinary maintenance. If reviewing for a security release, verify separately whether this refactor was triggered by an undisclosed bug in the old import/usage pattern.
Security signals we found
No security-relevant keywords in commit title or message
Refactoring only: import path changes and case normalization
Behavioral change in qr.py is a simplification, not a bounds-check or validation fix
No input validation, cryptographic, or memory-safety changes evident
Evidence from the diff
The patch refactors usage of foundation-ur and urtypes: (1) in datum_tool.py, wallet.py, psbt.py, and tests, it imports specific classes (BIP39, Account, Output, PSBT, Bytes) rather than the top-level urtypes package; (2) in datum_tool.py’s urobj_to_data, it compares ur_obj.type.upper() against uppercase constants instead of lower-case string literals; (3) in qr.py’s QRPartParser.result, it returns self.decoder.result directly rather than constructing a new UR object from the decoded type and cbor. The change in qr.py is behaviorally equivalent if the decoder already returns a UR instance. No security bug is described or directly observable in the diff.
Changed components
src/krux/pages/datum_tool.pysrc/krux/psbt.pysrc/krux/qr.pysrc/krux/wallet.pytests/pages/test_datum_tool.pytests/test_qr.pyInspect captured patch +36 / −38
diff --git a/src/krux/pages/datum_tool.py b/src/krux/pages/datum_tool.py
index b0fb7e6..503d4d9 100644
--- a/src/krux/pages/datum_tool.py
+++ b/src/krux/pages/datum_tool.py
@@ -78,23 +78,23 @@ SLOW_ENCODING_MAX_SIZE = 2**14 # base43,base58,bech32 not offered above this si
def urobj_to_data(ur_obj):
"""returns flatened data from a UR object. belongs in qr or qr_capture???"""
- import urtypes
-
- if ur_obj.type == "crypto-bip39":
- data = urtypes.crypto.BIP39.from_cbor(ur_obj.cbor).words
+ from urtypes.crypto.bip39 import BIP39
+ from urtypes.crypto.account import Account
+ from urtypes.crypto.output import Output
+ from urtypes.crypto.psbt import PSBT
+ from urtypes.bytes import Bytes
+
+ if ur_obj.type.upper() == "CRYPTO-BIP39":
+ data = BIP39.from_cbor(ur_obj.cbor).words
data = " ".join(data)
- elif ur_obj.type == "crypto-account":
- data = (
- urtypes.crypto.Account.from_cbor(ur_obj.cbor)
- .output_descriptors[0]
- .descriptor()
- )
- elif ur_obj.type == "crypto-output":
- data = urtypes.crypto.Output.from_cbor(ur_obj.cbor).descriptor()
- elif ur_obj.type == "crypto-psbt":
- data = urtypes.crypto.PSBT.from_cbor(ur_obj.cbor).data
- elif ur_obj.type == "bytes":
- data = urtypes.bytes.Bytes.from_cbor(ur_obj.cbor).data
+ elif ur_obj.type.upper() == "CRYPTO-ACCOUNT":
+ data = Account.from_cbor(ur_obj.cbor).output_descriptors[0].descriptor()
+ elif ur_obj.type.upper() == "CRYPTO-OUTPUT":
+ data = Output.from_cbor(ur_obj.cbor).descriptor()
+ elif ur_obj.type.upper() == "CRYPTO-PSBT":
+ data = PSBT.from_cbor(ur_obj.cbor).data
+ elif ur_obj.type.upper() == "BYTES":
+ data = Bytes.from_cbor(ur_obj.cbor).data
else:
data = None
return data
@@ -422,7 +422,8 @@ class DatumTool(Page):
"""Reusable handler for viewing a QR code"""
from ..qr import QR_CAPACITY_BYTE, QR_CAPACITY_ALPHANUMERIC, QR_CAPACITY_NUMERIC
from ..bbqr import encode_bbqr
- import urtypes
+ from urtypes.bytes import Bytes
+ from urtypes.crypto.psbt import PSBT
from ur.ur import UR
# Helper function to check if character is alphanumeric
@@ -515,9 +516,9 @@ class DatumTool(Page):
elif qr_fmt == FORMAT_UR:
ur_type = menu_opts[idx][1][1]
if ur_type == "bytes":
- encoded = UR(ur_type, urtypes.Bytes(encoded).to_cbor())
+ encoded = UR(ur_type, Bytes(encoded).to_cbor())
elif ur_type == "crypto-psbt":
- encoded = UR(ur_type, urtypes.PSBT(encoded).to_cbor())
+ encoded = UR(ur_type, PSBT(encoded).to_cbor())
# TODO: other urtypes
try:
diff --git a/src/krux/psbt.py b/src/krux/psbt.py
index 5c28d76..df9566c 100644
--- a/src/krux/psbt.py
+++ b/src/krux/psbt.py
@@ -22,8 +22,7 @@
import gc
from embit.psbt import PSBT, CompressMode
from ur.ur import UR
-import urtypes
-from urtypes.crypto import CRYPTO_PSBT
+from urtypes.crypto.psbt import PSBT as URTYPE_PSBT, CRYPTO_PSBT
from .baseconv import base_decode
from .krux_settings import t
from .settings import THIN_SPACE, ELLIPSIS
@@ -93,9 +92,7 @@ class PSBTSigner:
self.base_encoding = 64 # In case it is exported as QR code
elif isinstance(psbt_data, UR):
try:
- self.psbt = PSBT.parse(
- urtypes.crypto.PSBT.from_cbor(psbt_data.cbor).data
- )
+ self.psbt = PSBT.parse(URTYPE_PSBT.from_cbor(psbt_data.cbor).data)
self.ur_type = CRYPTO_PSBT
# self.base_encoding = 64
except:
@@ -538,7 +535,7 @@ class PSBTSigner:
return (
UR(
CRYPTO_PSBT.type,
- urtypes.crypto.PSBT(psbt_data).to_cbor(),
+ URTYPE_PSBT(psbt_data).to_cbor(),
),
self.qr_format,
)
diff --git a/src/krux/qr.py b/src/krux/qr.py
index 73d3b30..4d8e01b 100644
--- a/src/krux/qr.py
+++ b/src/krux/qr.py
@@ -210,9 +210,7 @@ class QRPartParser:
def result(self):
"""Returns the combined part data"""
if self.format == FORMAT_UR:
- from ur.ur import UR
-
- return UR(self.decoder.result.type, bytearray(self.decoder.result.cbor))
+ return self.decoder.result
if self.format == FORMAT_BBQR:
from .bbqr import decode_bbqr
diff --git a/src/krux/wallet.py b/src/krux/wallet.py
index cbc3fb3..df516e2 100644
--- a/src/krux/wallet.py
+++ b/src/krux/wallet.py
@@ -414,26 +414,28 @@ def parse_wallet(wallet_data):
# Check if wallet_data is a UR object without loading the UR module
if wallet_data.__class__.__name__ == "UR":
- import urtypes
-
# Try to parse as a Crypto-Output type
try:
- output = urtypes.crypto.Output.from_cbor(wallet_data.cbor)
+ from urtypes.crypto.output import Output
+
+ output = Output.from_cbor(wallet_data.cbor)
return Descriptor.from_string(output.descriptor()), None
except:
pass
# Try to parse as a Crypto-Account type
try:
- account = urtypes.crypto.Account.from_cbor(
- wallet_data.cbor
- ).output_descriptors[0]
+ from urtypes.crypto.account import Account
+
+ account = Account.from_cbor(wallet_data.cbor).output_descriptors[0]
return Descriptor.from_string(account.descriptor()), None
except:
pass
# Treat the UR as a generic UR bytes object and extract the data for further processing
- wallet_data = urtypes.Bytes.from_cbor(wallet_data.cbor).data
+ from urtypes.bytes import Bytes
+
+ wallet_data = Bytes.from_cbor(wallet_data.cbor).data
# Process as a string
wallet_data = (
diff --git a/tests/pages/test_datum_tool.py b/tests/pages/test_datum_tool.py
index 36b3c4d..6b24c2c 100644
--- a/tests/pages/test_datum_tool.py
+++ b/tests/pages/test_datum_tool.py
@@ -317,7 +317,7 @@ def test_datumtoolmenu_scan_qr_abort(m5stickv, mocker):
assert ctx.input.wait_for_button.call_count == len(BTN_SEQUENCE)
from ur.ur import UR
- from urtypes import Bytes
+ from urtypes.bytes import Bytes
# scan UR-QR (for coverage), then back out of datum tool
MULTISIG_DESCR = "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))#t2zpj2eu"
diff --git a/tests/test_qr.py b/tests/test_qr.py
index dcad8d4..d69fb03 100644
--- a/tests/test_qr.py
+++ b/tests/test_qr.py
@@ -115,7 +115,7 @@ def test_parser(mocker, m5stickv, tdata):
if num == 4:
# Multi-part UR
- assert parser.total_count() == len(parts) * 2
+ assert parser.processed_parts_count() == i + 1
else:
assert parser.total_count() == len(parts)
if parser.format == FORMAT_UR:
@@ -132,7 +132,7 @@ def test_parser(mocker, m5stickv, tdata):
parser.parse(parts[0])
if num == 4:
- assert parser.total_count() == len(parts) * 2
+ assert parser.processed_parts_count() == i + 1
else:
assert parser.total_count() == len(parts)
Why this scored 12/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.